diff --git a/.gitignore b/.gitignore index 3cffde1c..ab0e4ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ CMakeFiles/ __/ # Ignoring autogenerated files -*.cmake Makefile qrc_*.cpp *.qrc.depends diff --git a/.gitmodules b/.gitmodules index a9b2e1ba..981fff4d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ -[submodule "dependencies/external/protobuf"] - path = dependencies/external/protobuf - url = https://github.com/hyperion-project/protobuf.git [submodule "dependencies/external/rpi_ws281x"] path = dependencies/external/rpi_ws281x url = https://github.com/hyperion-project/rpi_ws281x.git branch = master +[submodule "dependencies/external/flatbuffers"] + path = dependencies/external/flatbuffers + url = https://github.com/google/flatbuffers diff --git a/.travis.yml b/.travis.yml index 6e2a6189..e85acbb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,19 +5,27 @@ cache: notifications: email: false language: cpp +services: + - docker matrix: include: - os: linux dist: trusty - sudo: required + env: + - DOCKER_TAG=ubuntu1604 + - DOCKER_NAME="Ubuntu 16.04" + - os: linux + dist: trusty + env: + - DOCKER_TAG=cross-qemu-rpistretch + - DOCKER_NAME="Raspberry Pi" - os: osx - osx_image: xcode7.3 + osx_image: xcode8.3 env: - HOMEBREW_CACHE=$HOME/brew-cache before_install: - ./.travis/travis_install.sh script: - ./.travis/travis_build.sh - - ./test/testrunner.sh after_success: - ./.travis/travis_deploy.sh diff --git a/.travis/travis_build.sh b/.travis/travis_build.sh index e1f864d0..ddeb569c 100755 --- a/.travis/travis_build.sh +++ b/.travis/travis_build.sh @@ -5,6 +5,7 @@ PLATFORM=x86 BUILD_TYPE=Debug +PACKAGES="" # Detect number of processor cores # default is 4 jobs @@ -16,32 +17,48 @@ elif [[ "$TRAVIS_OS_NAME" == 'linux' ]] then JOBS=$(nproc) fi +echo "compile jobs: ${JOBS:=4}" -# compile prepare -mkdir build || exit 1 -cd build - -# Compile hyperion for tags +# Determine cmake build type; tag builds are Release, else Debug [ -n "${TRAVIS_TAG:-}" ] && BUILD_TYPE=Release -# Compile hyperion for cron - take default settings +# Determine package creation; True for cron and tag builds +[ "${TRAVIS_EVENT_TYPE:-}" == 'cron' ] || [ -n "${TRAVIS_TAG:-}" ] && PACKAGES=package -# Compile for PR (no tag and no cron) +# Determie -dev appends to platform; [ "${TRAVIS_EVENT_TYPE:-}" != 'cron' -a -z "${TRAVIS_TAG:-}" ] && PLATFORM=${PLATFORM}-dev -cmake -DPLATFORM=$PLATFORM -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_INSTALL_PREFIX=/usr .. || exit 2 -if [[ "$TRAVIS_OS_NAME" == 'linux' ]] +# Build the package on osx +if [[ "$TRAVIS_OS_NAME" == 'osx' || "$TRAVIS_OS_NAME" == 'darwin' ]] then - # activate dispmanx and osx mocks - cmake -DENABLE_OSX=ON -DENABLE_DISPMANX=ON .. || exit 5 + # compile prepare + mkdir build || exit 1 + cd build + cmake -DPLATFORM=$PLATFORM -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_INSTALL_PREFIX=/usr .. || exit 2 + make -j ${JOBS} || exit 3 fi -echo "compile jobs: ${JOBS:=4}" -make -j ${JOBS} || exit 3 - -# Build the package on Linux +# Build the package with docker if [[ $TRAVIS_OS_NAME == 'linux' ]] then - make -j ${JOBS} package || exit 4 -fi + echo "Compile Hyperion with DOCKER_TAG = ${DOCKER_TAG} and friendly name DOCKER_NAME = ${DOCKER_NAME}" + # take ownership of deploy dir + mkdir $TRAVIS_BUILD_DIR/deploy + # run docker + docker run --rm \ + -v "${TRAVIS_BUILD_DIR}/deploy:/deploy" \ + -v "${TRAVIS_BUILD_DIR}:/source:ro" \ + hyperionorg/hyperion-ci:$DOCKER_TAG \ + /bin/bash -c "mkdir build && cp -r /source/. /build && + cd /build && mkdir build && cd build && + cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} .. || exit 2 && + make -j $(nproc) ${PACKAGES} || exit 3 && + echo '---> Copy binaries and packages to host folder: ${TRAVIS_BUILD_DIR}/deploy' && + cp -v /build/build/bin/h* /deploy/ 2>/dev/null || : && + cp -v /build/build/Hyperion-* /deploy/ 2>/dev/null || : && + exit 0; + exit 1 " || { echo "---> Hyperion compilation failed! Abort"; exit 4; } + # overwrite file owner to current user + sudo chown -fR $(stat -c "%U:%G" $TRAVIS_BUILD_DIR/deploy) $TRAVIS_BUILD_DIR/deploy +fi diff --git a/.travis/travis_deploy.sh b/.travis/travis_deploy.sh index 311727e6..2f41c285 100755 --- a/.travis/travis_deploy.sh +++ b/.travis/travis_deploy.sh @@ -1,10 +1,13 @@ #!/bin/bash -# sf_upload +# sf_upload sf_upload() { +echo "Uploading following files: ${1} +to dir /hyperion-project/${2}" + /usr/bin/expect <<-EOD - spawn scp $1 hyperionsf37@frs.sourceforge.net:/home/frs/project/hyperion-project/dev/$2 + spawn scp $1hyperionsf37@frs.sourceforge.net:/home/frs/project/hyperion-project/$2 expect "*(yes/no)*" send "yes\r" expect "*password:*" @@ -13,18 +16,49 @@ sf_upload() EOD } -deploylist="hyperion-2.0.0-Linux-x86.tar.gz" +# append current Date to filename (just packages no binaries) +appendDate() +{ + D=$(date +%Y-%m-%d) + for F in $TRAVIS_BUILD_DIR/deploy/Hy* + do + mv "$F" "${F%.*}-$D.${F##*.}" + done +} + +# append friendly name (just packages no binaries) +appendName() +{ + for F in $TRAVIS_BUILD_DIR/deploy/Hy* + do + mv "$F" "${F%.*}-($DOCKER_NAME).${F##*.}" + done +} + +# get all files to deploy (just packages no binaries) +getFiles() +{ + FILES="" + for f in $TRAVIS_BUILD_DIR/deploy/Hy*; + do FILES+="${f} "; + done; +} if [[ $TRAVIS_OS_NAME == 'linux' ]]; then - cd $TRAVIS_BUILD_DIR/build if [[ -n $TRAVIS_TAG ]]; then echo "tag upload" - sf_upload $deploylist release + appendName + appendDate + getFiles + sf_upload $FILES release elif [[ $TRAVIS_EVENT_TYPE == 'cron' ]]; then echo "cron upload" - sf_upload $deploylist alpha + appendName + appendDate + getFiles + sf_upload $FILES dev/alpha else - echo "PR can't be uploaded for security reasons" - sf_upload $deploylist pr + echo "Direct pushed no upload, PRs not possible" + #sf_upload $FILES pr fi fi diff --git a/.travis/travis_install.sh b/.travis/travis_install.sh index e974ab2e..414bd412 100755 --- a/.travis/travis_install.sh +++ b/.travis/travis_install.sh @@ -7,19 +7,19 @@ if [[ $TRAVIS_OS_NAME == 'osx' || $TRAVIS_OS_NAME == 'darwin' ]] then echo "Install OSX deps" - time brew update - time brew install qt5 || true - time brew install python3 || true - time brew install libusb || true - time brew install cmake || true - time brew install doxygen || true + brew update + brew install qt5 || true + brew upgrade python3 || true + brew upgrade libusb || true + brew upgrade cmake || true + brew install doxygen || true # install linux deps for hyperion compile elif [[ $TRAVIS_OS_NAME == 'linux' ]] then echo "Install linux deps" - sudo apt-get -qq update - sudo apt-get install -qq -y qtbase5-dev libqt5serialport5-dev libusb-1.0-0-dev python3-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev doxygen expect + #sudo apt-get -qq update + #sudo apt-get install -qq -y qtbase5-dev libqt5serialport5-dev libusb-1.0-0-dev python3-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev doxygen expect else echo "Unsupported platform: $TRAVIS_OS_NAME" exit 5 diff --git a/CMakeLists.txt b/CMakeLists.txt index facbd343..6615eee7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,9 +20,10 @@ SET ( DEFAULT_AMLOGIC OFF ) SET ( DEFAULT_DISPMANX OFF ) SET ( DEFAULT_OSX OFF ) SET ( DEFAULT_X11 OFF ) +SET ( DEFAULT_QT ON ) SET ( DEFAULT_WS281XPWM OFF ) SET ( DEFAULT_USE_SHARED_AVAHI_LIBS ON ) -SET ( DEFAULT_USE_SYSTEM_PROTO_LIBS OFF ) +SET ( DEFAULT_USE_SYSTEM_FLATBUFFERS_LIBS OFF ) SET ( DEFAULT_TESTS OFF ) IF ( ${CMAKE_SYSTEM} MATCHES "Linux" ) @@ -47,7 +48,7 @@ endif() if ( NOT DEFINED PLATFORM ) if ( "${CMAKE_SYSTEM_PROCESSOR}" MATCHES "x86" ) - SET( PLATFORM "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 ) @@ -61,7 +62,7 @@ if ( NOT DEFINED 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() @@ -95,9 +96,9 @@ elseif ( "${PLATFORM}" STREQUAL "amlogic" ) SET ( DEFAULT_AMLOGIC ON ) elseif ( "${PLATFORM}" STREQUAL "amlogic64" ) SET ( DEFAULT_AMLOGIC ON ) -elseif ( "${PLATFORM}" MATCHES "x86" ) +elseif ( "${PLATFORM}" MATCHES "x11" ) SET ( DEFAULT_X11 ON ) - if ( "${PLATFORM}" STREQUAL "x86-dev" ) + if ( "${PLATFORM}" STREQUAL "x11-dev" ) SET ( DEFAULT_AMLOGIC ON) SET ( DEFAULT_WS281XPWM ON ) endif() @@ -151,7 +152,8 @@ message(STATUS "ENABLE_USB_HID = ${ENABLE_USB_HID}") option(ENABLE_X11 "Enable the X11 grabber" ${DEFAULT_X11}) message(STATUS "ENABLE_X11 = ${ENABLE_X11}") -SET(ENABLE_QT5 ON) +option(ENABLE_QT "Enable the qt grabber" ${DEFAULT_QT}) +message(STATUS "ENABLE_QT = ${ENABLE_QT}") option(ENABLE_TESTS "Compile additional test applications" ${DEFAULT_TESTS}) message(STATUS "ENABLE_TESTS = ${ENABLE_TESTS}") @@ -159,9 +161,8 @@ message(STATUS "ENABLE_TESTS = ${ENABLE_TESTS}") option(ENABLE_PROFILER "enable profiler capabilities - not for release code" OFF) message(STATUS "ENABLE_PROFILER = ${ENABLE_PROFILER}") - -SET ( PROTOBUF_INSTALL_BIN_DIR ${CMAKE_BINARY_DIR}/proto ) -SET ( PROTOBUF_INSTALL_LIB_DIR ${CMAKE_BINARY_DIR}/proto ) +SET ( FLATBUFFERS_INSTALL_BIN_DIR ${CMAKE_BINARY_DIR}/flatbuf ) +SET ( FLATBUFFERS_INSTALL_LIB_DIR ${CMAKE_BINARY_DIR}/flatbuf ) # check all json files FILE ( GLOB_RECURSE HYPERION_SCHEMAS RELATIVE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/libsrc/*schema*.json ) @@ -230,6 +231,9 @@ CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11) CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") +if (CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-psabi") +endif() if(COMPILER_SUPPORTS_CXX11) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") elseif(COMPILER_SUPPORTS_CXX0X) @@ -246,7 +250,7 @@ if (UNIX AND NOT APPLE) endif () # add QT5 dependency -SET(QT_MIN_VERSION "5.2.0") +SET(QT_MIN_VERSION "5.5.0") find_package(Qt5 COMPONENTS Core Gui Network SerialPort REQUIRED) message( STATUS "Found Qt Version: ${Qt5Core_VERSION}" ) IF ( "${Qt5Core_VERSION}" VERSION_LESS "${QT_MIN_VERSION}" ) @@ -276,6 +280,9 @@ if (ENABLE_TESTS) add_subdirectory(test) endif () +# Add resources directory +add_subdirectory(resources) + # Add the doxygen generation directory add_subdirectory(doc) diff --git a/CompileHowto.md b/CompileHowto.md index 733aa7ef..205f9a47 100644 --- a/CompileHowto.md +++ b/CompileHowto.md @@ -1,4 +1,18 @@ -# Install the required tools and dependencies +# 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 compilation for Raspberry Pi (Raspbian stretch) + +To compile Hyperion for Ubuntu 16.04 (x64) or higher just execute the following command +``` +wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh +``` +To compile Hyperion for Raspberry Pi +``` +wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -t cross-qemu-rpistretch +``` +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 + +# The usual way ## Debian/Ubuntu/Win10LinuxSubsystem @@ -6,24 +20,13 @@ sudo apt-get update sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev libusb-1.0-0-dev python3-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev ``` -### Ubuntu 14.04 specific -You need a newer version of cmake (minimum 3.0.0). Install it from the ppa or website -``` -sudo apt-get install software-properties-common -sudo add-apt-repository ppa:george-edison55/cmake-3.x -sudo apt-get update && sudo apt-get upgrade -``` **on RPI you need the videocore IV headers** ``` sudo apt-get install libraspberrypi-dev ``` -**OSMC** -libraspberrypi-dev is not available, use this instead -``` -sudo apt-get install rbp-userland-dev-osmc -``` + **ATTENTION Win10LinuxSubsystem** we do not (/we can't) support using hyperion in linux subsystem of MS Windows 10, albeit some users tested it with success. Keep in mind to disable all linux specific led and grabber hardware via cmake. Because we use QT as framework in hyperion, serialport leds and network driven devices could work. @@ -71,24 +74,16 @@ sudo make install/strip sudo make uninstall # ... or run it from compile directory bin/hyperiond -# webui is located on localhost:8099 +# webui is located on localhost:8090 or 8091 ``` ### Download - Create hyperion directory and checkout the code from github - -You might want to add `--depth 1` to the `git` command if you only want to compile the current source and have no need for the entire git repository + Creates hyperion directory and checkout the code from github ``` export HYPERION_DIR="hyperion" -git clone --recursive https://github.com/hyperion-project/hyperion.ng.git "$HYPERION_DIR" -``` - -**Note:** If you forget the --recursive in above statement or you are updating an existing clone you need to clone the protobuf submodule by runnning the follwing two statements: -``` -git submodule init -git submodule update +git clone --recursive --depth 1 https://github.com/hyperion-project/hyperion.ng.git "$HYPERION_DIR" ``` ### Preparations @@ -110,7 +105,7 @@ cmake -DCMAKE_BUILD_TYPE=Release .. *Developers on x86* linux should use: ``` -cmake -DPLATFORM=x86-dev -DCMAKE_BUILD_TYPE=Release .. +cmake -DPLATFORM=x11-dev -DCMAKE_BUILD_TYPE=Release .. ``` To use framebuffer instead of dispmanx (for example on the *cubox-i*): diff --git a/CrossCompileHowto.txt b/CrossCompileHowto.txt index 73d565da..82adad84 100644 --- a/CrossCompileHowto.txt +++ b/CrossCompileHowto.txt @@ -12,7 +12,7 @@ sudo apt-get update sudo apt-get upgrade #TO-DO verify what is really required -#blacklist: protobuf-compiler lib32z1 lib32ncurses5 lib32bz2-1.0 zlib1g-dev +#blacklist: lib32z1 lib32ncurses5 lib32bz2-1.0 zlib1g-dev sudo apt-get -qq -y install git rsync cmake build-essential qtbase5-dev libqt5serialport5-dev libusb-1.0-0-dev python-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev echo 'PATH=$PATH:$HOME/raspberrypi/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin' >> .bashrc @@ -58,12 +58,12 @@ git clone --depth 1 git://github.com/raspberrypi/tools.git "$RASCROSS_DIR/tools" # get the Hyperion sources git clone --recursive https://github.com/hyperion-project/hyperion.ng.git "$HYPERION_DIR" -# do a native build (to build the protobuf compiler for the native platform) +# do a native build (to build the flatbuffers compiler for the native platform) mkdir -p "$NATIVE_BUILD_DIR" cmake -DENABLE_DISPMANX=OFF --build "$NATIVE_BUILD_DIR" "$HYPERION_DIR" # do the cross build -# specify the protoc export file to import the protobuf compiler from the native build +# specify the protoc export file to import the flatbuffers compiler from the native build mkdir -p "$TARGET_BUILD_DIR" cmake -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" -DIMPORT_PROTOC=$NATIVE_BUILD_DIR/protoc_export.cmake --build "$TARGET_BUILD_DIR" "$HYPERION_DIR" diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index e23b46d1..ddb8874e 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -18,6 +18,9 @@ // Define to enable the x11 grabber #cmakedefine ENABLE_X11 +// Define to enable the qt grabber +#cmakedefine ENABLE_QT + // Define to enable the spi-device #cmakedefine ENABLE_SPIDEV @@ -42,5 +45,3 @@ #define HYPERION_VERSION "${HYPERION_VERSION_MAJOR}.${HYPERION_VERSION_MINOR}.${HYPERION_VERSION_PATCH}" #define HYPERION_JSON_VERSION "1.0.0" - - diff --git a/README.md b/README.md index 4bc70795..ff1cfd7f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ HYPERION ======== +[![Build Status](https://travis-ci.org/hyperion-project/hyperion.ng.svg?branch=master)](https://travis-ci.org/hyperion-project/hyperion.ng) + This is a pre alpha development repository for the next major version of hyperion -------- @@ -32,7 +34,7 @@ If you need further support please open a topic at the our new forum! [Hyperion webpage/forum](https://www.hyperion-project.org). ## Requirements -* Debian 8, Ubuntu 14.04 or higher. Windows is not supported currently. +* Debian 9, Ubuntu 16.04 or higher. Windows is not supported currently. ## Building See [Compilehowto](CompileHowto.md) and [CrossCompileHowto](CrossCompileHowto.txt). diff --git a/assets/webconfig/css/hyperion.css b/assets/webconfig/css/hyperion.css index b648a05c..6919f11c 100644 --- a/assets/webconfig/css/hyperion.css +++ b/assets/webconfig/css/hyperion.css @@ -176,6 +176,9 @@ table label{margin:0} .checklist{list-style-type:none;padding-left:0px} .checklist li::before{content: "\f00c";font: normal normal normal 14px/1 FontAwesome;margin-right:5px;color:green;font-size:19px} +/* bgwhite */ +.bg-w{background-color: white;} + /*Modal icons*/ [class*="modal-icon"]{ padding:30px; @@ -993,4 +996,4 @@ input[type="radio"] .styled:checked + label::before { input[type="checkbox"] .styled:checked + label::after, input[type="radio"] .styled:checked + label::after { color: #fff; -} \ No newline at end of file +} diff --git a/assets/webconfig/i18n/cs.json b/assets/webconfig/i18n/cs.json index 988e14d8..8cc269fb 100644 --- a/assets/webconfig/i18n/cs.json +++ b/assets/webconfig/i18n/cs.json @@ -512,7 +512,7 @@ "edt_conf_fw_json_expl": "Jeden z cílů na jeden řádek. Obsahuje IP:PORT (Příklad: 127.0.0.1:19446)", "edt_conf_fw_json_itemtitle": "Json cíl", "edt_conf_fw_proto_title": "Seznam proto klientů", - "edt_conf_fw_proto_expl": "Jeden cíl na každý řádek. Obsahuje IP:PORT (Příklad: 127.0.0.1:19447)", + "edt_conf_fw_proto_expl": "Jeden cíl na každý řádek. Obsahuje IP:PORT (Příklad: 127.0.0.1:19401)", "edt_conf_fw_proto_itemtitle": "Proto cíl", "edt_conf_js_heading_title": "JSON Server", "edt_conf_ps_heading_title": "PROTO Server", @@ -753,4 +753,4 @@ "edt_conf_enum_SECAM": "SECAM", "general_speech_it": "Italština", "general_speech_cs": "Czech" -} \ No newline at end of file +} diff --git a/assets/webconfig/i18n/de.json b/assets/webconfig/i18n/de.json index cafcd74f..135d67a8 100644 --- a/assets/webconfig/i18n/de.json +++ b/assets/webconfig/i18n/de.json @@ -20,6 +20,7 @@ "general_comp_FORWARDER" : "Weiterleitung", "general_comp_UDPLISTENER" : "UDP Listener", "general_comp_BOBLIGHTSERVER" : "Boblight Server", + "general_comp_FLATBUFSERVER" : "Flatbuffers Server", "general_comp_GRABBER" : "Plattform Aufnahme", "general_comp_V4L" : "USB Aufnahme", "general_comp_LEDDEVICE" : "LED Hardware", @@ -27,6 +28,7 @@ "general_col_green" : "grün", "general_col_blue" : "blau", "general_button_savesettings" : "Einstellungen speichern", + "general_btn_yes" : "Ja", "general_btn_ok" : "OK", "general_btn_cancel" : "Abbrechen", "general_btn_continue" : "Fortfahren", @@ -46,7 +48,7 @@ "dashboard_infobox_label_latesthyp" : "Aktuellste Hyperion Version:", "dashboard_infobox_label_platform" : "Plattform:", "dashboard_infobox_label_instance" : "Instanz:", - "dashboard_infobox_label_ports" : "Ports (json|proto):", + "dashboard_infobox_label_ports" : "Port flatbuf:", "dashboard_infobox_message_updatewarning" : "Eine aktuellere Version von Hyperion ist verfügbar! (V$1)", "dashboard_infobox_message_updatesuccess" : "Du nutzt die aktuellste Version von Hyperion.", "dashboard_infobox_label_statush" : "Hyperion Status:", @@ -162,9 +164,9 @@ "conf_colors_smoothing_intro" : "Glätte den Farbverlauf und Helligkeitsänderungen um nicht von schnellen Übergängen abgelenkt zu werden.", "conf_colors_blackborder_intro" : "Ignoriere schwarze Balken, jeder Modus nutzt einen anderen Algorithmus um diese zu erkennen. Erhöhe die Schwelle, sollte es nicht funktionieren.", "conf_network_json_intro" : "Der JSON-RPC-Port dieser Hyperion-Instanz, wird genutzt zur Fernsteuerung.", - "conf_network_proto_intro" : "Der PROTO-Port dieser Hyperion-Instanz, wird genutzt für \"Bildstreams\" (HyperionScreenCap, Kodi Adddon, ...)", "conf_network_bobl_intro" : "Boblight Empfänger", "conf_network_udpl_intro" : "UDP Empfänger", + "conf_network_fbs_intro" : "Google Flatbuffers Empfänger. Wird genutzt für schnellen Bildempfang.", "conf_network_forw_intro" : "Leite alles an eine zweite Hyperion Instanz weiter, diese kann dann mit einer anderen LED Steuerung genutzt werden", "conf_logging_label_intro" : "Überprüfe die Meldungen im Prokotoll um zu erfahren was Hyperion gerade beschäftigt. Je nach eingestellter Protokoll-Stufe siehst du mehr oder weniger Informationen.", "conf_logging_btn_pbupload" : "Bericht für Supportanfrage hochladen", @@ -283,6 +285,7 @@ "InfoDialog_nowrite_text" : "Hyperion hat keinen Schreibzugriff auf die aktuell geladene Konfiguration. Bitte korrigiere die Dateizugriffsrechte um fortzufahren.", "InfoDialog_nowrite_foottext" : "Die Webkonfiguration wird automatisch wieder freigegeben, sobald das Problem behoben wurde!", "infoDialog_wizrgb_text" : "Deine RGB Byte Reihenfolge ist bereits richtig eingestellt.", + "infoDialog_writeimage_error_text": "Die ausgewählte Datei \"$1\" ist keine Bilddatei oder ist beschädigt! Bitte wähle eine andere Bilddatei aus.", "infoDialog_writeconf_error_text" : "Das speichern der Konfiguration ist fehlgeschlagen.", "infoDialog_import_jsonerror_text" : "Die ausgewählte Konfigurations-Datei \"$1\" ist keine .json Datei oder ist beschädigt! Fehlermeldung: ($2)", "infoDialog_import_hyperror_text" : "Die ausgewählte Konfigurations-Datei \"$1\" kann nicht importiert werden. Sie ist nicht kompatibel mit Hyperion 2.0 und höher!", @@ -341,11 +344,12 @@ "wiz_cc_morethanone" : "Du hast mehr als 1 Profil, bitte wähle das zu kalibrierende Profil", "wiz_cc_btn_stop" : "Stoppe Video", "wiz_cc_summary" : "Im folgenden eine Zusammenfassung deiner Einstellungen. Während du ein Video abspielst, kannst du hier weiter ausprobieren. Wenn du fertig bist, klicke auf speichern.", + "edt_dev_auth_key_title" : "Aurora API Schlüssel", "edt_dev_enum_subtract_minimum" : "Subtrahiere minimum", "edt_dev_enum_sub_min_warm_adjust" : "Minimale Anpassung: warm", "edt_dev_enum_white_off" : "Weiß ist aus", "edt_dev_general_heading_title" : "Allgemeine Einstellungen", - "edt_dev_general_ledCount_title" : "Anzahl Hardware LEDs", + "edt_dev_general_hardwareLedCount_title" : "Anzahl Hardware LEDs", "edt_dev_general_colorOrder_title" : "RGB Byte Reihenfolge", "edt_dev_general_rewriteTime_title" : "Aktualisierungszeit", "edt_dev_spec_header_title" : "Spezifische Einstellungen", @@ -415,6 +419,7 @@ "edt_conf_enum_PAL" : "PAL", "edt_conf_enum_NTSC" : "NTSC", "edt_conf_enum_SECAM" : "SECAM", + "edt_conf_enum_NO_CHANGE" : "Auto", "edt_conf_enum_logsilent" : "Stille", "edt_conf_enum_logwarn" : "Warnung", "edt_conf_enum_logverbose" : "Ausführlich", @@ -480,19 +485,11 @@ "edt_conf_smooth_continuousOutput_expl" : "Aktualisiere die LEDs, auch wenn das Bild sich nicht geändert hat.", "edt_conf_v4l2_heading_title" : "USB Aufnahme", "edt_conf_v4l2_device_title" : "Gerät", - "edt_conf_v4l2_device_expl" : "Der Pfad zum USB Aufnahmegerät.", - "edt_conf_v4l2_input_title" : "Eingang", - "edt_conf_v4l2_input_expl" : "Der Eingang des Pfades.", + "edt_conf_v4l2_device_expl" : "Der Pfad zum USB (v4l) Aufnahmegerät. Wähle 'auto' für automatische Erkennung. Beispiel: '/dev/video0'", "edt_conf_v4l2_standard_title" : "Videoformat", - "edt_conf_v4l2_standard_expl" : "Wähle das passende Videoformat deiner Region.", - "edt_conf_v4l2_width_title" : "Breite", - "edt_conf_v4l2_width_expl" : "Die Breite des Bildes. (-1 = Automatische Breitenbestimmung)", - "edt_conf_v4l2_height_title" : "Höhe", - "edt_conf_v4l2_height_expl" : "Die Höhes des Bildes. (-1 = Automatische Höhenbestimmung)", - "edt_conf_v4l2_frameDecimation_title" : "Bildverkleinerung", - "edt_conf_v4l2_frameDecimation_expl" : "Der Faktor der Bildverkleinerung", - "edt_conf_v4l2_sizeDecimation_title" : "Größenänderung", - "edt_conf_v4l2_sizeDecimation_expl" : "Der Faktor der Größenänderung", + "edt_conf_v4l2_standard_expl" : "Wähle das passende Videoformat deiner Region. Auf 'Auto' wird der gewählte Modus vom v4l interface beibehalten.", + "edt_conf_v4l2_sizeDecimation_title" : "Bildverkleinerung Faktor", + "edt_conf_v4l2_sizeDecimation_expl" : "Der Faktor der Bildverkleinerung ausgehend von der ursprünglichen Größe, 1 bedeutet keine Änderung (originales Bild).", "edt_conf_v4l2_cropLeft_title" : "Entferne links", "edt_conf_v4l2_cropLeft_expl" : "Anzahl der Pixel auf der linken Seite die vom Bild entfernt werden.", "edt_conf_v4l2_cropRight_title" : "Entferne rechts", @@ -502,7 +499,7 @@ "edt_conf_v4l2_cropBottom_title" : "Entferne unten", "edt_conf_v4l2_cropBottom_expl" : "Anzahl der Pixel auf der unteren Seite die vom Bild entfernt werden.", "edt_conf_v4l2_signalDetection_title" : "Signal Erkennung", - "edt_conf_v4l2_signalDetection_expl" : "Wenn aktiviert, wird die USB Aufnahme temporär bei \"kein Signal\" abgeschalten.", + "edt_conf_v4l2_signalDetection_expl" : "Wenn aktiviert, wird die USB Aufnahme temporär bei \"kein Signal\" abgeschalten. Das Bild muss dazu 4 Sekunden lang unter die Schwellwerte fallen.", "edt_conf_v4l2_redSignalThreshold_title" : "Rote Signalschwelle", "edt_conf_v4l2_redSignalThreshold_expl" : "Je höher die rote Schwelle je eher wird abgeschalten bei entsprechendem rot-Anteil.", "edt_conf_v4l2_greenSignalThreshold_title" : "Grüne Signalschwelle", @@ -517,21 +514,22 @@ "edt_conf_v4l2_sDVOffsetMax_expl" : "Signal Erkennungs-Bereich vertikal maximum (0.0-1.0)", "edt_conf_v4l2_sDHOffsetMax_title" : "Signal Erkennung HMax", "edt_conf_v4l2_sDHOffsetMax_expl" : "Signal Erkennungs-Bereich horizontal maximum (0.0-1.0)", + "edt_conf_instCapture_heading_title" : "Instance Aufnahme", + "edt_conf_instC_systemEnable_title" : "Aktiviere Plattform Aufnahme", + "edt_conf_instC_systemEnable_expl" : "Aktiviert die Plattform Aufnahme für diese LED Hardware Instanz", + "edt_conf_instC_v4lEnable_title" : "Aktiviere USB Aufnahme", + "edt_conf_instC_v4lEnable_expl" : "Aktiviert die USB Aufnahme für diese LED Hardware Instanz", "edt_conf_fg_heading_title" : "Plattform Aufnahme", "edt_conf_fg_type_title" : "Typ", "edt_conf_fg_type_expl" : "Art der Plattform Aufnahme, standard ist 'auto'", "edt_conf_fg_frequency_Hz_title" : "Aufnahmefrequenz", "edt_conf_fg_frequency_Hz_expl" : "Wie schnell neue Bilder aufgenommen werden.", - "edt_conf_fg_horizontalPixelDecimation_title" : "Horizontale Pixelreduzierung", - "edt_conf_fg_horizontalPixelDecimation_expl" : "Horizontale Pixelreduzierung (Faktor)", - "edt_conf_fg_useXGetImage_title" : "Nutze XGetImage", - "edt_conf_fg_useXGetImage_expl" : "XGetImage für aktuelle X11 desktops", "edt_conf_fg_width_title" : "Breite", "edt_conf_fg_width_expl" : "Verkleinere Bild auf dieser Breite, da das Rohmaterial viel Leistung benötigen würde.", "edt_conf_fg_height_title" : "Höhe", "edt_conf_fg_height_expl" : "Verkleinere Bild auf dieser Höhe, da das Rohmaterial viel Leistung benötigen würde.", - "edt_conf_fg_verticalPixelDecimation_title" : "Vertikale Pixelreduzierung", - "edt_conf_fg_verticalPixelDecimation_expl" : "Vertikale Pixelreduzierung (Faktor)", + "edt_conf_fg_pixelDecimation_title" : "Bildverkleinerung Faktor", + "edt_conf_fg_pixelDecimation_expl" : "Bildverkleinerung (Faktor) ausgehend von der original Größe. 1 für unveränderte/originale Größe.", "edt_conf_fg_device_title" : "Device", "edt_conf_fg_display_title" : "Display", "edt_conf_fg_display_expl" : "Gebe an von welchem Desktop aufgenommen werden soll. (Multi Monitor Setup)", @@ -563,16 +561,18 @@ "edt_conf_fw_json_expl" : "Ein Json Ziel pro Zeile. Bestehend aus IP:PORT (Beispiel: 127.0.0.1:19446)", "edt_conf_fw_json_itemtitle" : "Json Ziel", "edt_conf_fw_proto_title" : "Liste von Proto zielen", - "edt_conf_fw_proto_expl" : "Ein Proto Ziel pro Zeile. Bestehend aus IP:PORT (Beispiel: 127.0.0.1:19447)", + "edt_conf_fw_proto_expl" : "Ein Proto Ziel pro Zeile. Bestehend aus IP:PORT (Beispiel: 127.0.0.1:19401)", "edt_conf_fw_proto_itemtitle" : "Proto Ziel", "edt_conf_js_heading_title" : "JSON Server", - "edt_conf_ps_heading_title" : "PROTO Server", + "edt_conf_fbs_heading_title" : "Flatbuffers Server", + "edt_conf_fbs_timeout_title" : "Zeitüberschreitung", + "edt_conf_fbs_timeout_expl" : "Wenn für die angegebene Zeit keine Daten empfangen werden, wird die Komponente (vorübergehend) deaktiviert", "edt_conf_bobls_heading_title" : "Boblight Server", "edt_conf_udpl_heading_title" : "UDP Listener", "edt_conf_udpl_address_title" : "Adresse", "edt_conf_udpl_address_expl" : "Die Adresse auf der UDP Pakete akzeptiert werden.", "edt_conf_udpl_timeout_title" : "Zeitüberschreitung", - "edt_conf_udpl_timeout_expl" : "Wenn für die angegeben Zeit keine UDP Pakete empfangen werden, wird die Komponente (vorübergehend) deaktiviert", + "edt_conf_udpl_timeout_expl" : "Wenn für die angegebene Zeit keine UDP Pakete empfangen werden, wird die Komponente (vorübergehend) deaktiviert", "edt_conf_udpl_shared_title" : "Gemeinsam genutzt", "edt_conf_udpl_shared_expl" : "Wird gemeinsam über alle Hyperion Instanzen genutzt.", "edt_conf_webc_heading_title" : "Web Konfiguration", @@ -593,9 +593,14 @@ "edt_eff_smooth_updateFrequency" : "Glättung: Aktualisierungsfrequenz", "edt_eff_candle_header" : "Kerze", "edt_eff_candle_header_desc" : "Flackerndes Kerzenlicht", + "edt_eff_waves_header" : "Wellen", + "edt_eff_waves_header_desc" : "Gestalte Wellen aus Farbe! Mische dazu deine lieblings Farben und wähle einen Mittelpunkt.", + "edt_eff_gif_header" : "GIF's", + "edt_eff_gif_header_desc" : "Dieser Effekt spielt .gif Dateien ab. Bietet die Möglichkeit kleine GIF-Videos abzuspielen.", "edt_eff_police_header" : "Polizei", "edt_eff_police_header_desc" : "Lights like a police car in action", "edt_eff_fade_header" : "Farbübergang", + "edt_eff_fade_header_desc" : "Farbübergange für alle LED's", "edt_eff_rainbowmood_header" : "Regenbogen", "edt_eff_rainbowmood_header_desc" : "Alle LEDs Regenbogen Farbübergang", "edt_eff_knightrider_header" : "Knight Rider", @@ -603,18 +608,24 @@ "edt_eff_lightclock_header" : "Lichtuhr", "edt_eff_lightclock_header_desc" : "Eine echte Uhr als Licht! Passe die Farben von Stunden, Minuten, Sekunden deinen Vorstellungen an. Optional können 3/6/9/12 Uhr Markierungen aktiviert werden. Sollte die Uhr eine falsche Zeit anzeigen, überprüfe die Uhrzeit deines Systems.", "edt_eff_pacman_header" : "Pac-Man", + "edt_eff_pacman_header_desc" : "Klein gefräßig und gelb, wer wird überleben?", "edt_eff_moodblobs_header" : "Stimmungskugeln", + "edt_eff_moodblobs_header_desc" : "Entspannt den Abend beginnen mit langsam bewegenden Farbkugeln die ebenso sanft ihre Farbe verändern.", "edt_eff_swirl_header" : "Farbwirbel", "edt_eff_swirl_header_desc" : "Ein Wirbel mit frei wählbaren Farben. Die Farben werden gleichmäßig auf 360° aufgeteilt, dazwischen werden Farbübergänge berechnet. Zusätzlich kann ein zweiter Wirbel über den Ersten gelegt werden (Transparenz beachten!). Tipp: Eine Widerholung der selben Farbe erhöht deren \"größe\" und verringert den Bereich des Farbübergangs zu benachbarten Farben.", "edt_eff_random_header" : "Zufällig", - "edt_eff_runningdots_header" : "Rennende Punkte", + "edt_eff_random_header_desc" : "Pixel-Farb-Mix", "edt_eff_systemshutdown_header" : "Herunterfahren", + "edt_eff_systemshutdown_header_desc" : "Eine kurze Animation gefolgt von einem möglicherweise echten Herunterfahren des Systems", "edt_eff_snake_header" : "Schlange", + "edt_eff_snake_header_desc" : "Wo ist das Futter?", "edt_eff_sparks_header" : "Funken", "edt_eff_sparks_header_desc" : "Ein Sternenfunkeln, wahlweise in festgelegter Farbe oder zufällig. Passe Helligkeit, Sättigung und Geschwindigkeit an.", "edt_eff_traces_header" : "Farbspuren", "edt_eff_x-mas_header" : "Weihnachten", - "edt_eff_trails_header" : "Spuren", + "edt_eff_x-mas_header_desc" : "Ein Hauch von Weihnachten", + "edt_eff_trails_header" : "Sternschnuppen", + "edt_eff_trails_header_desc" : "In verschiedenen Farben, wünsch dir was!", "edt_eff_flag_header" : "Flaggen", "edt_eff_flag_header_desc" : "Verpasse deinen LEDs die Farben deines Landes. Du kannst mehr als eine Flagge auswählen, je nach Intervall werden diese dann abwechselnd angezeigt.", "edt_eff_enum_all" : "Alle", @@ -679,6 +690,7 @@ "edt_eff_customColor" : "Benutzerdefinierte Farbe", "edt_eff_randomCenter" : "Zufälliger Mittelpunkt", "edt_eff_enableSecondSwirl":"Zweiter Wirbel", + "edt_eff_reverseRandomTime":"Richtungswechsel alle", "edt_append_ns" : "ns", "edt_append_ms" : "ms", "edt_append_s" : "s", diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index f7fd7102..5bd4b134 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -20,6 +20,7 @@ "general_comp_FORWARDER" : "Forwarder", "general_comp_UDPLISTENER" : "UDP Listener", "general_comp_BOBLIGHTSERVER" : "Boblight Server", + "general_comp_FLATBUFSERVER" : "Flatbuffers Server", "general_comp_GRABBER" : "Platform Capture", "general_comp_V4L" : "USB Capture", "general_comp_LEDDEVICE" : "LED device", @@ -27,6 +28,7 @@ "general_col_green" : "green", "general_col_blue" : "blue", "general_button_savesettings" : "Save settings", + "general_btn_yes" : "Yes", "general_btn_ok" : "OK", "general_btn_cancel" : "Cancel", "general_btn_continue" : "Continue", @@ -46,7 +48,7 @@ "dashboard_infobox_label_latesthyp" : "Latest Hyperion version:", "dashboard_infobox_label_platform" : "Platform:", "dashboard_infobox_label_instance" : "Instance:", - "dashboard_infobox_label_ports" : "Ports (json|proto):", + "dashboard_infobox_label_ports" : "Port flatbuf:", "dashboard_infobox_message_updatewarning" : "A newer version of Hyperion is available! ($1)", "dashboard_infobox_message_updatesuccess" : "You run the latest version of Hyperion.", "dashboard_infobox_label_statush" : "Hyperion status:", @@ -162,9 +164,9 @@ "conf_colors_smoothing_intro" : "Smoothing flattens color/brightness changes to reduce annoying distraction.", "conf_colors_blackborder_intro" : "Skip black bars wherever they are. Each mode use another detection algorithm which is tuned for special situations. Higher the threshold if it doesn't work for you.", "conf_network_json_intro" : "The JSON-RPC-Port of this Hyperion instance, used for remote control.", - "conf_network_proto_intro" : "The PROTO-Port of this Hyperion instance, used for picture streams (HyperionScreenCap, Kodi Adddon, ...)", "conf_network_bobl_intro" : "Receiver for Boblight", "conf_network_udpl_intro" : "Receiver for UDP", + "conf_network_fbs_intro" : "Google Flatbuffers Receiver. Used for fast image transmission.", "conf_network_forw_intro" : "Forward all input to a second Hyperion instance which could be driven with another led controller", "conf_logging_label_intro" : "Area to check log messages, depending on loglevel setting you see more or less information.", "conf_logging_btn_pbupload" : "Upload report for support request", @@ -283,6 +285,7 @@ "InfoDialog_nowrite_text" : "Hyperion can't write to your current loaded configuration file. Please repair the file permissions to proceed.", "InfoDialog_nowrite_foottext" : "The WebUI will be unlocked automatically after you solved the problem!", "infoDialog_wizrgb_text" : "Your RGB Byte Order is already well adjusted.", + "infoDialog_writeimage_error_text": "The selected file \"$1\" is no image file or it's corrupted! Please select another image file.", "infoDialog_writeconf_error_text" : "Saving your configuration failed.", "infoDialog_import_jsonerror_text" : "The selected configuration file \"$1\" is no .json file or it's corrupted. Error message: ($2)", "infoDialog_import_hyperror_text" : "The selected configuration file \"$1\" can't be imported. It's not compatible with Hyperion 2.0 and higher!", @@ -341,12 +344,13 @@ "wiz_cc_morethanone" : "You have more than one profile, please choose the profile you want to calibrate.", "wiz_cc_btn_stop" : "Stop video", "wiz_cc_summary" : "A conclusion of your settings. During video playback, you could change or test values again. If you are done, click on save.", + "edt_dev_auth_key_title" : "Aurora API Key", "edt_dev_enum_subtract_minimum" : "Substract minimum", "edt_dev_enum_sub_min_warm_adjust" : "Min warm adjust", "edt_dev_enum_white_off" : "White off", "edt_dev_general_heading_title" : "General Settings", "edt_dev_general_name_title" : "Configuration name", - "edt_dev_general_ledCount_title" : "Count of all hardware LEDs", + "edt_dev_general_hardwareLedCount_title" : "Hardware LED count", "edt_dev_general_colorOrder_title" : "RGB byte order", "edt_dev_general_rewriteTime_title" : "Refresh time", "edt_dev_spec_header_title" : "Specific Settings", @@ -416,6 +420,7 @@ "edt_conf_enum_PAL" : "PAL", "edt_conf_enum_NTSC" : "NTSC", "edt_conf_enum_SECAM" : "SECAM", + "edt_conf_enum_NO_CHANGE" : "Auto", "edt_conf_enum_logsilent" : "Silent", "edt_conf_enum_logwarn" : "Warning", "edt_conf_enum_logverbose" : "Verbose", @@ -481,19 +486,11 @@ "edt_conf_smooth_continuousOutput_expl" : "Update the leds even there is no changed picture.", "edt_conf_v4l2_heading_title" : "USB Capture", "edt_conf_v4l2_device_title" : "Device", - "edt_conf_v4l2_device_expl" : "The path to the usb capture.", - "edt_conf_v4l2_input_title" : "Input", - "edt_conf_v4l2_input_expl" : "Input of this path.", + "edt_conf_v4l2_device_expl" : "The path to the usb capture interface. Set to 'auto' for auto detection. Example: '/dev/video0'", "edt_conf_v4l2_standard_title" : "Video standard", - "edt_conf_v4l2_standard_expl" : "Select the video standard for your region.", - "edt_conf_v4l2_width_title" : "Width", - "edt_conf_v4l2_width_expl" : "The width of the picture. (-1 = auto width)", - "edt_conf_v4l2_height_title" : "Height", - "edt_conf_v4l2_height_expl" : "The height of the picture. (-1 = auto height)", - "edt_conf_v4l2_frameDecimation_title" : "Frame decimation", - "edt_conf_v4l2_frameDecimation_expl" : "The factor of frame decimation", + "edt_conf_v4l2_standard_expl" : "Select the video standard for your region. 'Auto' keeps the chosen one from v4l interface", "edt_conf_v4l2_sizeDecimation_title" : "Size decimation", - "edt_conf_v4l2_sizeDecimation_expl" : "The factor of size decimation", + "edt_conf_v4l2_sizeDecimation_expl" : "The factor of size decimation. 1 means no decimation (keep original size)", "edt_conf_v4l2_cropLeft_title" : "Crop left", "edt_conf_v4l2_cropLeft_expl" : "Count of pixels on the left side that are removed from the picture.", "edt_conf_v4l2_cropRight_title" : "Crop right", @@ -503,7 +500,7 @@ "edt_conf_v4l2_cropBottom_title" : "Crop bottom", "edt_conf_v4l2_cropBottom_expl" : "Count of pixels on the bottom side that are removed from the picture.", "edt_conf_v4l2_signalDetection_title" : "Signal detection", - "edt_conf_v4l2_signalDetection_expl" : "If enabled, usb capture will be temporarily disabled when no signal was found.", + "edt_conf_v4l2_signalDetection_expl" : "If enabled, usb capture will be temporarily disabled when no signal was found. This will happen when the picture fall below the threshold value for a period of 4 seconds.", "edt_conf_v4l2_redSignalThreshold_title" : "Red signal threshold", "edt_conf_v4l2_redSignalThreshold_expl" : "Darkens low red values (recognized as black)", "edt_conf_v4l2_greenSignalThreshold_title" : "Green signal threshold", @@ -518,21 +515,22 @@ "edt_conf_v4l2_sDVOffsetMax_expl" : "Signal detection area vertical maximum (0.0-1.0)", "edt_conf_v4l2_sDHOffsetMax_title" : "Signal Detection HMax", "edt_conf_v4l2_sDHOffsetMax_expl" : "Signal detection area horizontal maximum (0.0-1.0)", + "edt_conf_instCapture_heading_title" : "Instance Capture", + "edt_conf_instC_systemEnable_title" : "Enable platform capture", + "edt_conf_instC_systemEnable_expl" : "Enables the platform capture for this led hardware instance", + "edt_conf_instC_v4lEnable_title" : "Enable USB capture", + "edt_conf_instC_v4lEnable_expl" : "Enables the USB capture for this led hardware instance", "edt_conf_fg_heading_title" : "Platform Capture", "edt_conf_fg_type_title" : "Type", "edt_conf_fg_type_expl" : "Type of platform capture, default is 'auto'", "edt_conf_fg_frequency_Hz_title" : "Capture frequency", "edt_conf_fg_frequency_Hz_expl" : "How fast new pictures are captured", - "edt_conf_fg_horizontalPixelDecimation_title" : "Horizontal pixel decimation", - "edt_conf_fg_horizontalPixelDecimation_expl" : "Horizontal pixel decimation (factor)", "edt_conf_fg_width_title" : "Width", "edt_conf_fg_width_expl" : "Shrink picture to this width, as raw picture needs a lot of cpu time.", "edt_conf_fg_height_title" : "Height", "edt_conf_fg_height_expl" : "Shrink picture to this height, as raw picture needs a lot of cpu time.", - "edt_conf_fg_useXGetImage_title" : "Use XGetImage", - "edt_conf_fg_useXGetImage_expl" : "XGetImage for newer X11 desktops", - "edt_conf_fg_verticalPixelDecimation_title" : "Vertical pixel decimation", - "edt_conf_fg_verticalPixelDecimation_expl" : "Vertical pixel decimation (factor)", + "edt_conf_fg_pixelDecimation_title" : "Picture decimation", + "edt_conf_fg_pixelDecimation_expl" : "Reduce picture size (factor) based on original size. A factor of 1 means no change", "edt_conf_fg_device_title" : "Device", "edt_conf_fg_display_title" : "Display", "edt_conf_fg_display_expl" : "Select which desktop should be captured (multi monitor setup)", @@ -564,10 +562,12 @@ "edt_conf_fw_json_expl" : "One json target per line. Contains IP:PORT (Example: 127.0.0.1:19446)", "edt_conf_fw_json_itemtitle" : "Json target", "edt_conf_fw_proto_title" : "List of proto clients", - "edt_conf_fw_proto_expl" : "One proto target per line. Contains IP:PORT (Example: 127.0.0.1:19447)", + "edt_conf_fw_proto_expl" : "One proto target per line. Contains IP:PORT (Example: 127.0.0.1:19401)", "edt_conf_fw_proto_itemtitle" : "Proto target", "edt_conf_js_heading_title" : "JSON Server", - "edt_conf_ps_heading_title" : "PROTO Server", + "edt_conf_fbs_heading_title" : "Flatbuffers Server", + "edt_conf_fbs_timeout_title" : "Timeout", + "edt_conf_fbs_timeout_expl" : "If no data are received for the given period, the component will be (soft) disabled.", "edt_conf_bobls_heading_title" : "Boblight Server", "edt_conf_udpl_heading_title" : "UDP Listener", "edt_conf_udpl_address_title" : "Address", @@ -592,6 +592,10 @@ "edt_eff_smooth_custom" : "Enable smoothing", "edt_eff_smooth_time_ms" : "Smoothing time", "edt_eff_smooth_updateFrequency" : "Smoothing update frequency", + "edt_eff_waves_header" : "Waves", + "edt_eff_waves_header_desc" : "Waves of color! Choose your colors, rotation time, direction reverse and more.", + "edt_eff_gif_header" : "GIF's", + "edt_eff_gif_header_desc" : "This effect plays .gif files, provide a simple video like loop as effect.", "edt_eff_candle_header" : "Candle", "edt_eff_candle_header_desc" : "Shimmering candles", "edt_eff_police_header" : "Police", @@ -605,20 +609,27 @@ "edt_eff_lightclock_header" : "Light Clock", "edt_eff_lightclock_header_desc" : "A real clock as light! Adjsut the colors of hours, minute, seconds. A optional 3/6/9/12 o'clock marker is also available. In case the clock is wrong, you need to check your system clock.", "edt_eff_pacman_header" : "Pac-Man", + "edt_eff_pacman_header_desc" : "Small hungry and yellow. Who will survive?", "edt_eff_moodblobs_header" : "Mood Blobs", + "edt_eff_moodblobs_header_desc" : "Relax at the evening with slow moving and color changing blobs.", "edt_eff_swirl_header" : "Color Swirl", "edt_eff_swirl_header_desc" : "A swirl with custom colors. Colors are even spread to 360°, in between colors shifts will be calcualted. Additional you can add a second swirl on top, be aware that you need partly transparency! Hint: A reapeat of the same color results in a \"hugher\" color area and a reduced color shift area.", "edt_eff_random_header" : "Random", - "edt_eff_runningdots_header" : "Running Dots", + "edt_eff_random_header_desc" : "Pixel Dot, dot, dot...", "edt_eff_systemshutdown_header" : "System Shutdown", + "edt_eff_systemshutdown_header_desc" : "A short animation with probably a real system shutdown", "edt_eff_snake_header" : "Snake", + "edt_eff_snake_header_desc" : "Where is something to eat?", "edt_eff_sparks_header" : "Sparks", "edt_eff_sparks_header_desc" : "Star-Sparking, choose between a static color or random. You could also adjust brightness, staturation and speed.", "edt_eff_traces_header" : "Color Traces", + "edt_eff_traces_header_desc" : "Requires redesign", "edt_eff_x-mas_header" : "X-Mas", - "edt_eff_trails_header" : "Trails", + "edt_eff_x-mas_header_desc" : "Touch of christmas", + "edt_eff_trails_header" : "Falling stars", + "edt_eff_trails_header_desc" : "Colored stars that fall from top to bottom", "edt_eff_flag_header" : "Flags", - "edt_eff_flag_header_desc" : "Let your leds shine bright in the colors of your country. You could select more then one flag, they will change based on interval time.", + "edt_eff_flag_header_desc" : "Let your leds shine bright in the colors of your country. You could select more than one flag, they will change based on interval time.", "edt_eff_enum_all" : "All", "edt_eff_enum_all-together" : "All together", "edt_eff_enum_list" : "LED List", @@ -630,6 +641,8 @@ "edt_eff_colorcount" : "Color length", "edt_eff_rotationtime" : "Rotation time", "edt_eff_sleeptime" : "Sleep time", + "edt_eff_image" : "Image file", + "edt_eff_fps" : "Frames per seconds", "edt_eff_reversedirection" : "Reverse direction", "edt_eff_fadeintime" : "Fade in time", "edt_eff_fadeouttime" : "Fade out time", @@ -681,6 +694,7 @@ "edt_eff_customColor" : "Custom Color", "edt_eff_randomCenter" : "Random Center", "edt_eff_enableSecondSwirl":"Second Swirl", + "edt_eff_reverseRandomTime":"Reverse every", "edt_append_ns" : "ns", "edt_append_ms" : "ms", "edt_append_s" : "s", diff --git a/assets/webconfig/i18n/it.json b/assets/webconfig/i18n/it.json index f44f8b88..1a85d8cf 100644 --- a/assets/webconfig/i18n/it.json +++ b/assets/webconfig/i18n/it.json @@ -512,7 +512,7 @@ "edt_conf_fw_json_expl": "Una destinazione json per riga. Contiene IP:PORTA:(Esempio: 127.0.0.1:19446)", "edt_conf_fw_json_itemtitle": "Destinatario json", "edt_conf_fw_proto_title": "Lista dei client proto", - "edt_conf_fw_proto_expl": "Una destinazione proto per riga. Contiene IP:PORTA:(Esempio: 127.0.0.1:19447)", + "edt_conf_fw_proto_expl": "Una destinazione proto per riga. Contiene IP:PORTA:(Esempio: 127.0.0.1:19401)", "edt_conf_fw_proto_itemtitle": "Destinatario proto", "edt_conf_js_heading_title": "Server JSON", "edt_conf_ps_heading_title": "Server PROTO", @@ -753,4 +753,4 @@ "edt_conf_enum_SECAM": "SECAM", "general_speech_it": "Italiano", "general_speech_cs": "Czech" -} \ No newline at end of file +} diff --git a/assets/webconfig/i18n/qqq.json b/assets/webconfig/i18n/qqq.json index 9722d677..40ac58cc 100644 --- a/assets/webconfig/i18n/qqq.json +++ b/assets/webconfig/i18n/qqq.json @@ -1,10 +1,10 @@ { "@metadata": { "authors": [ - "brindosch" + "brindosch, paulchen-panther" ], "project" : "Hyperion WebUI string docu", - "last-updated": "2016-11-30" + "last-updated": "2019-01-02" }, "edt_msg_error_notset" : " When a property is not set", "edt_msg_error_notempty" : "When a string must not be empty", @@ -40,8 +40,14 @@ "edt_msg_button_add_row_title" : "Title on Add Row buttons. $1 = This key takes one variable: The title of object to add", "edt_msg_button_move_down_title" : "Title on Move Down buttons", "edt_msg_button_move_up_title" : "Title on Move Up buttons", - "edt_msg_button_delete_row_titlet" : "Title on Delete Row buttons. $1 = This key takes one variable: The title of object to delete", + "edt_msg_button_delete_row_title" : "Title on Delete Row buttons. $1 = This key takes one variable: The title of object to delete", "edt_msg_button_delete_row_title_short" : "Title on Delete Row buttons, short version (no parameter with the object title)", "edt_msg_button_collapse" : "Title on Collapse buttons", - "edt_msg_button_expand" : "Title on Expand buttons" -} \ No newline at end of file + "edt_msg_button_expand" : "Title on Expand buttons", + "edt_msg_error_date" : "When a date is in incorrect format. $1 = This key takes one variable: The valid format", + "edt_msg_error_time" : "When a time is in incorrect format. $1 = This key takes one variable: The valid format", + "edt_msg_error_datetime_local" : "When a datetime-local is in incorrect format. $1 = This key takes one variable: The valid format", + "edt_msg_error_invalid_epoch" : "When a integer date is less than 1 January 1970", + "edt_msg_flatpickr_toggle_button" : "Title on Flatpickr toggle buttons", + "edt_msg_flatpickr_clear_button" : "Title on Flatpickr clear buttons" +} diff --git a/assets/webconfig/img/hyperion/ssdp_icon.png b/assets/webconfig/img/hyperion/ssdp_icon.png new file mode 100644 index 00000000..ebd86fbe Binary files /dev/null and b/assets/webconfig/img/hyperion/ssdp_icon.png differ diff --git a/assets/webconfig/index.html b/assets/webconfig/index.html index f222ff77..650d5036 100644 --- a/assets/webconfig/index.html +++ b/assets/webconfig/index.html @@ -31,6 +31,9 @@ + + + @@ -176,7 +179,10 @@ diff --git a/assets/webconfig/js/content_dashboard.js b/assets/webconfig/js/content_dashboard.js index ca0cae1e..b43ca531 100644 --- a/assets/webconfig/js/content_dashboard.js +++ b/assets/webconfig/js/content_dashboard.js @@ -1,6 +1,6 @@ $(document).ready( function() { performTranslation(); - + function newsCont(t,e,l) { var h = '
'; @@ -10,7 +10,7 @@ $(document).ready( function() { h += '

'; $('#dash_news').append(h); } - + function createNews(d) { for(var i = 0; i'; @@ -45,30 +45,40 @@ $(document).ready( function() { $('#dash_news').html(h); }); } - + //getNews(); - + function updateComponents() { - var components = serverInfo.components; + var components = comps; components_html = ""; for ( idx=0; idx'; + if(components[idx].name != "ALL") + components_html += ''+$.i18n('general_comp_'+components[idx].name)+''; } $("#tab_components").html(components_html); - + //info - $('#dash_statush').html(serverInfo.hyperion.off? ''+$.i18n('general_btn_off')+'':''+$.i18n('general_btn_on')+''); - $('#btn_hsc').html(serverInfo.hyperion.off? '' : ''); + hyperion_enabled = true; + + components.forEach( function(obj) { + if (obj.name == "ALL") + { + hyperion_enabled = obj.enabled + } + }); + + $('#dash_statush').html(hyperion_enabled ? ''+$.i18n('general_btn_on')+'' : ''+$.i18n('general_btn_off')+''); + $('#btn_hsc').html(hyperion_enabled ? '' : ''); } - + // add more info $('#dash_leddevice').html(serverInfo.ledDevices.active); $('#dash_currv').html(currentVersion); $('#dash_instance').html(serverConfig.general.name); - $('#dash_ports').html(jsonPort+' | '+serverConfig.protoServer.port); - + $('#dash_ports').html(serverConfig.flatbufServer.port); + $.get( "https://raw.githubusercontent.com/hyperion-project/hyperion.ng/master/version.json", function( data ) { parsedUpdateJSON = JSON.parse(data); latestVersion = parsedUpdateJSON[0].versionnr; @@ -76,13 +86,13 @@ $(document).ready( function() { var cleanCurrentVersion = currentVersion.replace(/\./g, ''); // $('#dash_latev').html(latestVersion); - + // if ( cleanCurrentVersion < cleanLatestVersion ) // $('#versioninforesult').html('
'+$.i18n('dashboard_infobox_message_updatewarning', latestVersion)+'
'); // else $('#versioninforesult').html('
'+$.i18n('dashboard_infobox_message_updatesuccess')+'
'); }); - + //determine platform var grabbers = serverInfo.grabbers.available; var html = ""; @@ -97,16 +107,16 @@ $(document).ready( function() { html += 'Amlogic'; else html += 'Framebuffer'; - - $('#dash_platform').html(html); - - + + $('#dash_platform').html(html); + + //interval update updateComponents(); - $(hyperion).on("cmd-serverinfo",updateComponents); - + $(hyperion).on("components-updated",updateComponents); + if(showOptHelp) createHintH("intro", $.i18n('dashboard_label_intro'), "dash_intro"); - + removeOverlay(); -}); \ No newline at end of file +}); diff --git a/assets/webconfig/js/content_effects.js b/assets/webconfig/js/content_effects.js index 1c054ddf..75053262 100644 --- a/assets/webconfig/js/content_effects.js +++ b/assets/webconfig/js/content_effects.js @@ -121,7 +121,10 @@ $(document).ready( function() { } //interval update - $(hyperion).on("cmd-serverinfo",updateEffectlist); + $(hyperion).on("cmd-effects-update", function(event){ + serverInfo.effects = event.response.data.effects + updateEffectlist(); + }); removeOverlay(); }); diff --git a/assets/webconfig/js/content_effectsconfigurator.js b/assets/webconfig/js/content_effectsconfigurator.js index 9d7eae61..29cbfd22 100644 --- a/assets/webconfig/js/content_effectsconfigurator.js +++ b/assets/webconfig/js/content_effectsconfigurator.js @@ -2,6 +2,7 @@ $(document).ready( function() { performTranslation(); var oldDelList = []; var effectName = ""; + var imageData = ""; var effects_editor = null; var effectPy = ""; var testrun; @@ -31,9 +32,29 @@ $(document).ready( function() { function triggerTestEffect() { testrun = true; var args = effects_editor.getEditor('root.args'); - requestTestEffect(effectName, ":/effects/" + effectPy.slice(1), JSON.stringify(args.getValue())); + requestTestEffect(effectName, ":/effects/" + effectPy.slice(1), JSON.stringify(args.getValue()), imageData); }; + // Specify upload handler for image files + JSONEditor.defaults.options.upload = function(type, file, cbs) { + var fileReader = new FileReader(); + + //check file + if (!file.type.startsWith('image')) { + imageData = ""; + cbs.failure('File upload error'); + // TODO clear file dialog. + showInfoDialog('error', "", $.i18n('infoDialog_writeimage_error_text', file.name)); + return; + } + + fileReader.onload = function () { + imageData = this.result.split(',')[1]; + cbs.success(file.name); + }; + + fileReader.readAsDataURL(file); + }; $("#effectslist").off().on("change", function(event) { if(effects_editor != null) @@ -48,6 +69,7 @@ $(document).ready( function() { effectPy = ':'; effectPy += effects[idx].schemaContent.script; + imageData = ""; $("#name-input").trigger("change"); $("#eff_desc").html(createEffHint($.i18n(effects[idx].schemaContent.title),$.i18n(effects[idx].schemaContent.title+'_desc'))); @@ -70,6 +92,7 @@ $(document).ready( function() { }); }); + // disable or enable control elements $("#name-input").on('change keyup', function(event) { effectName = $(this).val(); if ($(this).val() == '') { @@ -81,8 +104,9 @@ $(document).ready( function() { } }); + // Save Effect $('#btn_write').off().on('click',function() { - requestWriteEffect(effectName,effectPy,JSON.stringify(effects_editor.getValue())); + requestWriteEffect(effectName,effectPy,JSON.stringify(effects_editor.getValue()),imageData); $(hyperion).one("cmd-create-effect", function(event) { if (event.response.success) showInfoDialog('success', "", $.i18n('infoDialog_effconf_created_text', effectName)); @@ -93,21 +117,25 @@ $(document).ready( function() { }); + // Start test $('#btn_start_test').off().on('click',function() { triggerTestEffect(); }); + // Stop test $('#btn_stop_test').off().on('click',function() { requestPriorityClear(); testrun = false; }); + // Continuous test $('#btn_cont_test').off().on('click',function() { toggleClass('#btn_cont_test', "btn-success", "btn-danger"); }); + // Delete Effect $('#btn_delete').off().on('click',function() { - var name = $("#effectsdellist").val(); + var name = $("#effectsdellist").val().split("_")[1]; requestDeleteEffect(name); $(hyperion).one("cmd-delete-effect", function(event) { if (event.response.success) @@ -115,11 +143,13 @@ $(document).ready( function() { }); }); + // disable or enable Delete Effect Button $('#effectsdellist').off().on('change', function(){ $(this).val() == null ? $('#btn_edit, #btn_delete').prop('disabled',true) : ""; $(this).val().startsWith("int_") ? $('#btn_delete').prop('disabled',true) : $('#btn_delete').prop('disabled',false); }); + // Load Effect $('#btn_edit').off().on('click', function(){ var name = $("#effectsdellist").val().replace("ext_",""); @@ -155,15 +185,18 @@ $(document).ready( function() { //create basic effect list var effects = serverSchema.properties.effectSchemas.internal for(var idx=0; idx -1) - hideEl(["device","verticalPixelDecimation","horizontalPixelDecimation","useXGetImage"]); + hideEl(["device","pixelDecimation"]); else if(grabbers.indexOf('x11') > -1) hideEl(["device","width","height"]); else if(grabbers.indexOf('osx') > -1 ) - hideEl(["device","verticalPixelDecimation","horizontalPixelDecimation","useXGetImage"]); + hideEl(["device","pixelDecimation"]); else if(grabbers.indexOf('amlogic') > -1) - hideEl(["verticalPixelDecimation","horizontalPixelDecimation","useXGetImage"]); + hideEl(["pixelDecimation"]); }); - + removeOverlay(); }); - diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index a45ad775..8b061f9a 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -1,6 +1,4 @@ $(document).ready( function() { - var uiLock = false; - var prevSess = 0; loadContentTo("#container_connection_lost","connection_lost"); loadContentTo("#container_restart","restart"); @@ -8,70 +6,52 @@ $(document).ready( function() { $(hyperion).on("cmd-serverinfo",function(event){ serverInfo = event.response.info; + // comps + comps = event.response.info.components + $(hyperion).trigger("ready"); - - if (serverInfo.hyperion.config_modified) - $("#hyperion_reload_notify").fadeIn("fast"); - else - $("#hyperion_reload_notify").fadeOut("fast"); - if (serverInfo.hyperion.off) - $("#hyperion_disabled_notify").fadeIn("fast"); - else - $("#hyperion_disabled_notify").fadeOut("fast"); - - if (!serverInfo.hyperion.config_writeable) - { - showInfoDialog('uilock',$.i18n('InfoDialog_nowrite_title'),$.i18n('InfoDialog_nowrite_text')); - $('#wrapper').toggle(false); - uiLock = true; - } - else if (uiLock) - { - $("#modal_dialog").modal('hide'); - $('#wrapper').toggle(true); - uiLock = false; - } - - var sess = serverInfo.hyperion.sessions; - if (sess.length != prevSess) - { - wSess = []; - prevSess = sess.length; - for(var i = 0; i 1) - $('#btn_instanceswitch').toggle(true); - else - $('#btn_instanceswitch').toggle(false); - } + }); + if (serverInfo.hyperion.enabled) + $("#hyperion_disabled_notify").fadeOut("fast"); + else + $("#hyperion_disabled_notify").fadeIn("fast"); + + updateSessions(); }); // end cmd-serverinfo - $(hyperion).one("cmd-sysinfo", function(event) { + $(hyperion).on("cmd-sessions-update", function(event) { + serverInfo.sessions = event.response.data; + updateSessions(); + }); + + $(hyperion).on("cmd-sysinfo", function(event) { requestServerInfo(); sysInfo = event.response.info; currentVersion = sysInfo.hyperion.version; }); - + $(hyperion).one("cmd-config-getschema", function(event) { - serverSchema = event.response.result; + serverSchema = event.response.info; requestServerConfig(); - + schema = serverSchema.properties; }); - $(hyperion).one("cmd-config-getconfig", function(event) { - serverConfig = event.response.result; + $(hyperion).on("cmd-config-getconfig", function(event) { + serverConfig = event.response.info; requestSysInfo(); - + showOptHelp = serverConfig.general.showOptHelp; }); @@ -82,15 +62,44 @@ $(document).ready( function() { $(hyperion).on("open",function(event){ requestServerConfigSchema(); }); - + $(hyperion).one("ready", function(event) { loadContent(); }); - - $("#btn_hyperion_reload").on("click", function(){ - initRestart(); + + $(hyperion).on("cmd-adjustment-update", function(event) { + serverInfo.adjustment = event.response.data }); - + + $(hyperion).on("cmd-videomode-update", function(event) { + serverInfo.videomode = event.response.data.videomode + }); + + $(hyperion).on("cmd-components-update", function(event) { + let obj = event.response.data + + // notfication in index + if (obj.name == "ALL") + { + if(obj.enabled) + $("#hyperion_disabled_notify").fadeOut("fast"); + else + $("#hyperion_disabled_notify").fadeIn("fast"); + } + + comps.forEach((entry, index) => { + if (entry.name === obj.name){ + comps[index] = obj; + } + }); + // notify the update + $(hyperion).trigger("components-updated"); + }); + + $(hyperion).on("cmd-effects-update", function(event){ + serverInfo.effects = event.response.data.effects + }); + $(".mnava").bind('click.menu', function(e){ loadContent(e); window.scrollTo(0, 0); @@ -105,4 +114,3 @@ $(function(){ $(this).toggleClass('active inactive'); }); }); - diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 575c76e7..81d02e64 100644 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -470,10 +470,10 @@ $(document).ready(function() { // create led device selection ledDevices = serverInfo.ledDevices.available - devRPiSPI = ['apa102', 'ws2801', 'lpd6803', 'lpd8806', 'p9813', 'sk6812spi', 'sk6822spi', 'ws2812spi']; + devRPiSPI = ['apa102', 'apa104', 'ws2801', 'lpd6803', 'lpd8806', 'p9813', 'sk6812spi', 'sk6822spi', 'ws2812spi']; devRPiPWM = ['ws281x']; devRPiGPIO = ['piblaster']; - devNET = ['atmoorb', 'fadecandy', 'philipshue', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udph801', 'udpraw']; + devNET = ['atmoorb', 'fadecandy', 'philipshue', 'aurora', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udph801', 'udpraw']; devUSB = ['adalight', 'dmx', 'atmo', 'hyperionusbasp', 'lightpack', 'multilightpack', 'paintpack', 'rawhid', 'sedu', 'tpm2', 'karate']; var optArr = [[]]; diff --git a/assets/webconfig/js/content_network.js b/assets/webconfig/js/content_network.js index 94f1fa30..7c747121 100644 --- a/assets/webconfig/js/content_network.js +++ b/assets/webconfig/js/content_network.js @@ -1,24 +1,26 @@ $(document).ready( function() { performTranslation(); - + + var conf_editor_net = null; var conf_editor_json = null; var conf_editor_proto = null; + var conf_editor_fbs = null; var conf_editor_bobl = null; var conf_editor_udpl = null; var conf_editor_forw = null; - + if(showOptHelp) { //jsonserver $('#conf_cont').append(createRow('conf_cont_json')) $('#conf_cont_json').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_js_heading_title"), 'editor_container_jsonserver', 'btn_submit_jsonserver')); $('#conf_cont_json').append(createHelpTable(schema.jsonServer.properties, $.i18n("edt_conf_js_heading_title"))); - - //protoserver - $('#conf_cont').append(createRow('conf_cont_proto')) - $('#conf_cont_proto').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_ps_heading_title"), 'editor_container_protoserver', 'btn_submit_protoserver')); - $('#conf_cont_proto').append(createHelpTable(schema.protoServer.properties, $.i18n("edt_conf_ps_heading_title"))); - + + //flatbufserver + $('#conf_cont').append(createRow('conf_cont_flatbuf')) + $('#conf_cont_flatbuf').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_fbs_heading_title"), 'editor_container_fbserver', 'btn_submit_fbserver')); + $('#conf_cont_flatbuf').append(createHelpTable(schema.flatbufServer.properties, $.i18n("edt_conf_fbs_heading_title"))); + //boblight $('#conf_cont').append(createRow('conf_cont_bobl')) $('#conf_cont_bobl').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_bobls_heading_title"), 'editor_container_boblightserver', 'btn_submit_boblightserver')); @@ -41,10 +43,10 @@ $(document).ready( function() { { $('#conf_cont').addClass('row'); $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_js_heading_title"), 'editor_container_jsonserver', 'btn_submit_jsonserver')); - $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_ps_heading_title"), 'editor_container_protoserver', 'btn_submit_protoserver')); + $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_fbs_heading_title"), 'editor_container_fbserver', 'btn_submit_fbserver')); $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_bobls_heading_title"), 'editor_container_boblightserver', 'btn_submit_boblightserver')); $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_udpl_heading_title"), 'editor_container_udplistener', 'btn_submit_udplistener')); - if(storedAccess != 'default') + if(storedAccess != 'default') $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_fw_heading_title"), 'editor_container_forwarder', 'btn_submit_forwarder')); } @@ -61,19 +63,19 @@ $(document).ready( function() { requestWriteConfig(conf_editor_json.getValue()); }); - //proto - conf_editor_proto = createJsonEditor('editor_container_protoserver', { - protoServer : schema.protoServer + //flatbuffer + conf_editor_fbs = createJsonEditor('editor_container_fbserver', { + flatbufServer : schema.flatbufServer }, true, true); - conf_editor_proto.on('change',function() { - conf_editor_proto.validate().length ? $('#btn_submit_protoserver').attr('disabled', true) : $('#btn_submit_protoserver').attr('disabled', false); + conf_editor_fbs.on('change',function() { + conf_editor_fbs.validate().length ? $('#btn_submit_fbserver').attr('disabled', true) : $('#btn_submit_fbserver').attr('disabled', false); }); - - $('#btn_submit_protoserver').off().on('click',function() { - requestWriteConfig(conf_editor_proto.getValue()); + + $('#btn_submit_fbserver').off().on('click',function() { + requestWriteConfig(conf_editor_fbs.getValue()); }); - + //boblight conf_editor_bobl = createJsonEditor('editor_container_boblightserver', { boblightServer : schema.boblightServer @@ -82,11 +84,11 @@ $(document).ready( function() { conf_editor_bobl.on('change',function() { conf_editor_bobl.validate().length ? $('#btn_submit_boblightserver').attr('disabled', true) : $('#btn_submit_boblightserver').attr('disabled', false); }); - + $('#btn_submit_boblightserver').off().on('click',function() { requestWriteConfig(conf_editor_bobl.getValue()); }); - + //udplistener conf_editor_udpl = createJsonEditor('editor_container_udplistener', { udpListener : schema.udpListener @@ -95,11 +97,11 @@ $(document).ready( function() { conf_editor_udpl.on('change',function() { conf_editor_udpl.validate().length ? $('#btn_submit_udplistener').attr('disabled', true) : $('#btn_submit_udplistener').attr('disabled', false); }); - + $('#btn_submit_udplistener').off().on('click',function() { requestWriteConfig(conf_editor_udpl.getValue()); }); - + if(storedAccess != 'default') { //forwarder @@ -115,17 +117,16 @@ $(document).ready( function() { requestWriteConfig(conf_editor_forw.getValue()); }); } - + //create introduction if(showOptHelp) { createHint("intro", $.i18n('conf_network_json_intro'), "editor_container_jsonserver"); - createHint("intro", $.i18n('conf_network_proto_intro'), "editor_container_protoserver"); + createHint("intro", $.i18n('conf_network_fbs_intro'), "editor_container_fbserver"); createHint("intro", $.i18n('conf_network_bobl_intro'), "editor_container_boblightserver"); createHint("intro", $.i18n('conf_network_udpl_intro'), "editor_container_udplistener"); createHint("intro", $.i18n('conf_network_forw_intro'), "editor_container_forwarder"); } - + removeOverlay(); }); - diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index 32d36c99..eab3dca6 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -86,22 +86,6 @@ $(document).ready(function() { requestSetColor(rgb.r, rgb.g, rgb.b,duration); } - function updateRemote() - { - if ($('#componentsbutton').length == 0) - { - $(hyperion).off("cmd-serverinfo",updateRemote); - } - else - { - updateInputSelect(); - updateLedMapping(); - updateVideoMode(); - updateComponents(); - updateEffectlist(); - } - } - function updateInputSelect() { $('.sstbody').html(""); @@ -123,6 +107,7 @@ $(document).ready(function() { var priority = prios[i].priority; var compId = prios[i].componentId; var duration = prios[i].duration_ms/1000; + var value = "0,0,0"; var btn_type = "default"; var btn_text = $.i18n('remote_input_setsource_btn'); var btn_state = "enabled"; @@ -145,13 +130,16 @@ $(document).ready(function() { if(ip) origin += '
'+$.i18n('remote_input_ip')+' '+ip+''; + if("value" in prios[i]) + value = prios[i].value.RGB; + switch (compId) { case "EFFECT": owner = $.i18n('remote_effects_label_effects')+' '+owner; break; case "COLOR": - owner = $.i18n('remote_color_label_color')+' '+'
'; + owner = $.i18n('remote_color_label_color')+' '+'
'; break; case "GRABBER": owner = $.i18n('general_comp_GRABBER')+': ('+owner+')'; @@ -165,6 +153,9 @@ $(document).ready(function() { case "UDPLISTENER": owner = $.i18n('general_comp_UDPLISTENER'); break; + case "FLATBUFSERVER": + owner = $.i18n('general_comp_FLATBUFSERVER'); + break; } if(duration && compId != "GRABBER" && compId != "PROTOSERVER") @@ -195,7 +186,7 @@ $(document).ready(function() { function updateLedMapping() { - mapping = serverInfo.ledMAppingType; + mapping = serverInfo.imageToLedMappingType; $('#mappingsbutton').html(""); for(var ix = 0; ix < mappingList.length; ix++) @@ -211,16 +202,27 @@ $(document).ready(function() { function updateComponents() { - components = serverInfo.components; + components = comps; + var hyperionEnabled = true; + components.forEach( function(obj) { + if (obj.name == "ALL") + { + hyperionEnabled = obj.enabled + } + }); + // create buttons $('#componentsbutton').html(""); for ( idx=0; idx'+led.index+''; - } + leds_html = ''; + leds_html += ''; + $('#leds_canvas').html(leds_html); - - if($('#leds_toggle_num').hasClass('btn-success')) - $('.led_num').toggle(true); - - if($('#leds_toggle').hasClass('btn-danger')) - $('.led').toggle(false); - - $('#image_preview').attr("width" , canvas_width-1); - $('#image_preview').attr("height", canvas_height-1); + + imageCanvasNodeCtx = document.getElementById("image_preview_canv").getContext("2d"); + ledsCanvasNodeCtx = document.getElementById("leds_preview_canv").getContext("2d"); + create2dPaths(); + printLedsToCanvas(); + resetImage() } - // ------------------------------------------------------------------ - $('#leds_toggle_num').off().on("click", function() { - $('.led_num').toggle(); - toggleClass('#leds_toggle_num', "btn-danger", "btn-success"); - }); // ------------------------------------------------------------------ + $('#leds_toggle_num').off().on("click", function() { + toggleLedsNum = !toggleLedsNum + toggleClass('#leds_toggle_num', "btn-danger", "btn-success"); + }); + // ------------------------------------------------------------------ + $('#leds_toggle').off().on("click", function() { - $('.led').toggle(); + toggleLeds = !toggleLeds + ledsCanvasNodeCtx.clear(); toggleClass('#leds_toggle', "btn-success", "btn-danger"); }); @@ -106,14 +200,14 @@ $(document).ready(function() { if ( imageStreamActive ) { requestLedImageStop(); - $('#image_preview').removeAttr("src"); + resetImage(); } else { requestLedImageStart(); } }); - + // ------------------------------------------------------------------ $(hyperion).on("cmd-ledcolors-ledstream-update",function(event){ if (!modalOpened) @@ -122,15 +216,10 @@ $(document).ready(function() { } else { - ledColors = (event.response.result.leds); - for(var idx=0; idx {1} {2}
'};String.format=function(){for(var t=arguments[0],e=1;e .progress-bar').removeClass("progress-bar-"+t.settings.type),t.settings.type=i[e],this.$ele.addClass("alert-"+i[e]).find('[data-notify="progressbar"] > .progress-bar').addClass("progress-bar-"+i[e]);break;case"icon":var n=this.$ele.find('[data-notify="icon"]');"class"==t.settings.icon_type.toLowerCase()?n.removeClass(t.settings.content.icon).addClass(i[e]):(n.is("img")||n.find("img"),n.attr("src",i[e]));break;case"progress":var a=t.settings.delay-t.settings.delay*(i[e]/100);this.$ele.data("notify-delay",a),this.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i[e]).css("width",i[e]+"%");break;case"url":this.$ele.find('[data-notify="url"]').attr("href",i[e]);break;case"target":this.$ele.find('[data-notify="url"]').attr("target",i[e]);break;default:this.$ele.find('[data-notify="'+e+'"]').html(i[e])}var o=this.$ele.outerHeight()+parseInt(t.settings.spacing)+parseInt(t.settings.offset.y);t.reposition(o)},close:function(){t.close()}}},buildNotify:function(){var e=this.settings.content;this.$ele=t(String.format(this.settings.template,this.settings.type,e.title,e.message,e.url,e.target)),this.$ele.attr("data-notify-position",this.settings.placement.from+"-"+this.settings.placement.align),this.settings.allow_dismiss||this.$ele.find('[data-notify="dismiss"]').css("display","none"),(this.settings.delay<=0&&!this.settings.showProgressbar||!this.settings.showProgressbar)&&this.$ele.find('[data-notify="progressbar"]').remove()},setIcon:function(){"class"==this.settings.icon_type.toLowerCase()?this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon):this.$ele.find('[data-notify="icon"]').is("img")?this.$ele.find('[data-notify="icon"]').attr("src",this.settings.content.icon):this.$ele.find('[data-notify="icon"]').append('Notify Icon')},styleURL:function(){this.$ele.find('[data-notify="url"]').css({backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)",height:"100%",left:"0px",position:"absolute",top:"0px",width:"100%",zIndex:this.settings.z_index+1}),this.$ele.find('[data-notify="dismiss"]').css({position:"absolute",right:"10px",top:"5px",zIndex:this.settings.z_index+2})},placement:function(){var e=this,s=this.settings.offset.y,i={display:"inline-block",margin:"0px auto",position:this.settings.position?this.settings.position:"body"===this.settings.element?"fixed":"absolute",transition:"all .5s ease-in-out",zIndex:this.settings.z_index},n=!1,a=this.settings;switch(t('[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])').each(function(){return s=Math.max(s,parseInt(t(this).css(a.placement.from))+parseInt(t(this).outerHeight())+parseInt(a.spacing))}),1==this.settings.newest_on_top&&(s=this.settings.offset.y),i[this.settings.placement.from]=s+"px",this.settings.placement.align){case"left":case"right":i[this.settings.placement.align]=this.settings.offset.x+"px";break;case"center":i.left=0,i.right=0}this.$ele.css(i).addClass(this.settings.animate.enter),t.each(Array("webkit","moz","o","ms",""),function(t,s){e.$ele[0].style[s+"AnimationIterationCount"]=1}),t(this.settings.element).append(this.$ele),1==this.settings.newest_on_top&&(s=parseInt(s)+parseInt(this.settings.spacing)+this.$ele.outerHeight(),this.reposition(s)),t.isFunction(e.settings.onShow)&&e.settings.onShow.call(this.$ele),this.$ele.one(this.animations.start,function(){n=!0}).one(this.animations.end,function(){t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)}),setTimeout(function(){n||t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)},600)},bind:function(){var e=this;if(this.$ele.find('[data-notify="dismiss"]').on("click",function(){e.close()}),this.$ele.mouseover(function(){t(this).data("data-hover","true")}).mouseout(function(){t(this).data("data-hover","false")}),this.$ele.data("data-hover","false"),this.settings.delay>0){e.$ele.data("notify-delay",e.settings.delay);var s=setInterval(function(){var t=parseInt(e.$ele.data("notify-delay"))-e.settings.timer;if("false"===e.$ele.data("data-hover")&&"pause"==e.settings.mouse_over||"pause"!=e.settings.mouse_over){var i=(e.settings.delay-t)/e.settings.delay*100;e.$ele.data("notify-delay",t),e.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i).css("width",i+"%")}t<=-e.settings.timer&&(clearInterval(s),e.close())},e.settings.timer)}},close:function(){var e=this,s=parseInt(this.$ele.css(this.settings.placement.from)),i=!1;this.$ele.data("closing","true").addClass(this.settings.animate.exit),e.reposition(s),t.isFunction(e.settings.onClose)&&e.settings.onClose.call(this.$ele),this.$ele.one(this.animations.start,function(){i=!0}).one(this.animations.end,function(){t(this).remove(),t.isFunction(e.settings.onClosed)&&e.settings.onClosed.call(this)}),setTimeout(function(){i||(e.$ele.remove(),e.settings.onClosed&&e.settings.onClosed(e.$ele))},600)},reposition:function(e){var s=this,i='[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])',n=this.$ele.nextAll(i);1==this.settings.newest_on_top&&(n=this.$ele.prevAll(i)),n.each(function(){t(this).css(s.settings.placement.from,e),e=parseInt(e)+parseInt(s.settings.spacing)+t(this).outerHeight()})}}),t.notify=function(t,s){var i=new e(this,t,s);return i.notify},t.notifyDefaults=function(e){return s=t.extend(!0,{},s,e)},t.notifyClose=function(e){"undefined"==typeof e||"all"==e?t("[data-notify]").find('[data-notify="dismiss"]').trigger("click"):t('[data-notify-position="'+e+'"]').find('[data-notify="dismiss"]').trigger("click")}}); \ No newline at end of file diff --git a/assets/webconfig/js/lib/jsoneditor.js b/assets/webconfig/js/lib/jsoneditor.js index 3cc8fe5a..8fa013d4 100755 --- a/assets/webconfig/js/lib/jsoneditor.js +++ b/assets/webconfig/js/lib/jsoneditor.js @@ -5982,24 +5982,8 @@ JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({ if(!this.preview_value) return; var self = this; - - var mime = this.preview_value.match(/^data:([^;,]+)[;,]/); - if(mime) mime = mime[1]; - if(!mime) mime = 'unknown'; - var file = this.uploader.files[0]; - this.preview.innerHTML = 'Type: '+mime+', Size: '+file.size+' bytes'; - if(mime.substr(0,5)==="image") { - this.preview.innerHTML += '
'; - var img = document.createElement('img'); - img.style.maxWidth = '100%'; - img.style.maxHeight = '100px'; - img.src = this.preview_value; - this.preview.appendChild(img); - } - - this.preview.innerHTML += '
'; var uploadButton = this.getButton('Upload', 'upload', 'Upload'); this.preview.appendChild(uploadButton); uploadButton.addEventListener('click',function(event) { @@ -6036,6 +6020,11 @@ JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({ } }); }); + + if(this.jsoneditor.options.auto_upload || this.schema.options.auto_upload) { + uploadButton.dispatchEvent(new MouseEvent('click')); + this.preview.removeChild(uploadButton); + } }, enable: function() { if(this.uploader) this.uploader.disabled = false; @@ -6825,10 +6814,7 @@ JSONEditor.defaults.themes.bootstrap3 = JSONEditor.AbstractTheme.extend({ }, getButton: function(text, icon, title) { var el = this._super(text, icon, title); - if(icon.className.includes("fa-times")) - el.className += 'btn btn-sm btn-danger'; - else - el.className += 'btn btn-sm btn-primary'; + el.className += 'btn btn-default'; return el; }, getTable: function() { diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index fd20c904..0e9ca232 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -7,7 +7,7 @@ function removeOverlay() function reload() { - location.reload(); + location.reload(); } function storageComp() @@ -48,6 +48,27 @@ function debugMessage(msg) } } +function updateSessions() +{ + var sess = serverInfo.sessions; + if (sess.length) + { + wSess = []; + for(var i = 0; i 1) + $('#btn_instanceswitch').toggle(true); + else + $('#btn_instanceswitch').toggle(false); + } +} + function validateDuration(d) { if(typeof d === "undefined" || d < 0) @@ -70,12 +91,12 @@ function getHashtag() } } -function loadContent(event) +function loadContent(event, forceRefresh) { var tag; - + if(typeof event != "undefined") - { + { tag = event.currentTarget.hash; tag = tag.substr(tag.indexOf("#") + 1); setStorage('lasthashtag', tag, true); @@ -83,7 +104,7 @@ function loadContent(event) else tag = getHashtag(); - if(prevTag != tag) + if(forceRefresh || prevTag != tag) { prevTag = tag; $("#page-content").off(); @@ -130,7 +151,7 @@ function setClassByBool(obj,enable,class1,class2) } function showInfoDialog(type,header,message) -{ +{ if (type=="success"){ $('#id_body').html(''); if(header == "") @@ -143,12 +164,12 @@ function showInfoDialog(type,header,message) $('#id_body').append('

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

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

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

'); $('#id_footer').html(''); - } + } else if (type == "select"){ $('#id_body').html('Redefine ambient light!'); $('#id_footer').html(''); @@ -178,10 +199,10 @@ function showInfoDialog(type,header,message) $('#id_body').append('

'+header+'

'); $('#id_body').append(message); - + if(type == "select" || type == "iswitch") $('#id_body').append(''); - + $("#modal_dialog").modal({ backdrop : "static", keyboard: false, @@ -193,14 +214,14 @@ function createHintH(type, text, container) { if(type = "intro") tclass = "introd"; - + $('#'+container).prepend('

'+text+'


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

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

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

'+text+'
'); else if(type == "wizard") @@ -247,8 +268,8 @@ function valValue(id,value,min,max) { if(typeof max === 'undefined' || max == "") max = 999999; - - if(Number(value) > Number(max)) + + if(Number(value) > Number(max)) { $('#'+id).val(max); showInfoDialog("warning","",$.i18n('edt_msg_error_maximum_incl',max)); @@ -260,7 +281,7 @@ function valValue(id,value,min,max) showInfoDialog("warning","",$.i18n('edt_msg_error_minimum_incl',min)); return min; } - return value; + return value; } function readImg(input,cb) @@ -294,12 +315,10 @@ function createJsonEditor(container,schema,setconfig,usePanel,arrayre) { $('#'+container).off(); $('#'+container).html(""); - - //JSONEditor.plugins.selectize.enable = true; - + if (typeof arrayre === 'undefined') arrayre = true; - + var editor = new JSONEditor(document.getElementById(container), { theme: 'bootstrap3', @@ -338,18 +357,18 @@ function createJsonEditor(container,schema,setconfig,usePanel,arrayre) } function buildWL(link,linkt,cl) -{ +{ var baseLink = "https://docs.hyperion-project.org/"; var lang; - + if(typeof linkt == "undefined") linkt = "Placeholder"; - + if(storedLang == "de" || navigator.locale == "de") lang = "de"; else lang = "en"; - + if(cl === true) { linkt = $.i18n(linkt); @@ -366,7 +385,7 @@ function rgbToHex(rgb) return "#" + ("0" + parseInt(rgb[0],10).toString(16)).slice(-2) + ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[2],10).toString(16)).slice(-2); + ("0" + parseInt(rgb[2],10).toString(16)).slice(-2); } else debugMessage('rgbToHex: Given rgb is no array or has wrong length'); @@ -381,6 +400,57 @@ function hexToRgb(hex) { } : null; } +/* + Show a notification + @param type Valid types are "info","success","warning","danger" + @param message The message to show + @param title A title (optional) + */ +function showNotification(type, message, title="") +{ + if(title == "") + { + switch(type) + { + case "info": + title = $.i18n('infoDialog_general_info_title'); + break; + case "success": + title = $.i18n('infoDialog_general_success_title'); + break; + case "warning": + title = $.i18n('infoDialog_general_warning_title'); + break; + case "danger": + title = $.i18n('infoDialog_general_error_title'); + break; + } + } + + $.notify({ + // options + title: title, + message: message + },{ + // settings + type: type, + animate: { + enter: 'animated fadeInRight', + exit: 'animated fadeOutRight' + }, + mouse_over : 'pause', + template: '' + }); +} function createCP(id, color, cb) { @@ -388,7 +458,7 @@ function createCP(id, color, cb) color = rgbToHex(color); else if(color == "undefined") color = "#AA3399"; - + if(color.startsWith("#")) { $('#'+id).colorpicker({ @@ -425,7 +495,7 @@ function createTable(hid, bid, cont, bless, tclass) var table = document.createElement('table'); var thead = document.createElement('thead'); var tbody = document.createElement('tbody'); - + table.className = "table"; if(bless === true) table.className += " borderless"; @@ -438,30 +508,30 @@ function createTable(hid, bid, cont, bless, tclass) if(hid != "") table.appendChild(thead); table.appendChild(tbody); - + $('#'+cont).append(table); } // Creates a table row // @param array list :innerHTML content for / // @param bool head :if null or false it's body -// @param bool align :if null or false no alignment +// @param bool align :if null or false no alignment // // @return : with or as child(s) function createTableRow(list, head, align) { var row = document.createElement('tr'); - + for(var i = 0; i < list.length; i++) { if(head === true) var el = document.createElement('th'); else var el = document.createElement('td'); - + if(align) el.style.verticalAlign = "middle"; - + el.innerHTML = list[i]; row.appendChild(el); } @@ -483,7 +553,7 @@ function createOptPanel(phicon, phead, bodyid, footerid) pfooter.className = "btn btn-primary"; pfooter.setAttribute("id", footerid); pfooter.innerHTML = ''+$.i18n('general_button_savesettings'); - + return createPanel(phead, "", pfooter, "panel-default", bodyid); } @@ -506,30 +576,35 @@ function createHelpTable(list, phead){ var thead = document.createElement('thead'); var tbody = document.createElement('tbody'); list = sortProperties(list); - + phead = ''+phead+' '+$.i18n("conf_helptable_expl"); - + table.className = 'table table-hover borderless'; - + thead.appendChild(createTableRow([$.i18n('conf_helptable_option'), $.i18n('conf_helptable_expl')], true, false)); - + for (key in list) { if(list[key].access != 'system') { + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in list[key] && "hidden" in list[key].options && (list[key].options.hidden)) + continue; var text = list[key].title.replace('title', 'expl'); tbody.appendChild(createTableRow([$.i18n(list[key].title), $.i18n(text)], false, false)); - + if(list[key].items && list[key].items.properties) { var ilist = sortProperties(list[key].items.properties); for (ikey in ilist) { - + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in ilist[ikey] && "hidden" in ilist[ikey].options && (ilist[ikey].options.hidden)) + continue; var itext = ilist[ikey].title.replace('title', 'expl'); tbody.appendChild(createTableRow([$.i18n(ilist[ikey].title), $.i18n(itext)], false, false)); } - } + } } } table.appendChild(thead); @@ -544,42 +619,42 @@ function createPanel(head, body, footer, type, bodyid){ var phead = document.createElement('div'); var pbody = document.createElement('div'); var pfooter = document.createElement('div'); - + cont.className = "col-lg-6"; - + if(typeof type == 'undefined') type = 'panel-default'; - + p.className = 'panel '+type; phead.className = 'panel-heading'; pbody.className = 'panel-body'; pfooter.className = 'panel-footer'; - + phead.innerHTML = head; - + if(typeof bodyid != 'undefined') { pfooter.style.textAlign = 'right'; pbody.setAttribute("id", bodyid) } - + if(typeof body != 'undefined' && body != "") pbody.appendChild(body); - + if(typeof footer != 'undefined') pfooter.appendChild(footer); - + p.appendChild(phead); p.appendChild(pbody); - + if(typeof footer != 'undefined') { pfooter.style.textAlign = "right"; p.appendChild(pfooter); } - + cont.appendChild(p); - + return cont; } @@ -589,12 +664,12 @@ function createSelGroup(group) el.setAttribute('label', group); return el; } - + function createSelOpt(opt, title) { var el = document.createElement('option'); el.setAttribute('value', opt); - if (typeof title == 'undefined') + if (typeof title == 'undefined') el.innerHTML = opt; else el.innerHTML = title; diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index c501b8a8..b5165c84 100644 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -58,7 +58,7 @@ $('#wizp2_body').append('
'+$.i18n('edt_append_s')+'
'); $('#wizp2_body').append(''); $('#wizp2_body').append('
'); - $('#wizp2_footer').html('') + $('#wizp2_footer').html('') //open modal $("#wizard_modal").modal({ @@ -155,7 +155,6 @@ resetWizard(); serverConfig.device.colorOrder = new_rgb_order; requestWriteConfig({"device" : serverConfig.device}); - setTimeout(initRestart, 100); }); } @@ -416,7 +415,7 @@ $('#wizp1_body').html('

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

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

'); $('#wizp1_footer').html(''); $('#wizp2_body').html('
'); - $('#wizp2_footer').html('') + $('#wizp2_footer').html('') //open modal $("#wizard_modal").modal({ @@ -500,7 +499,6 @@ $('#btn_wiz_save').off().on('click',function() { requestWriteConfig(wiz_editor.getValue()); resetWizard(); - setTimeout(initRestart, 200); }); wiz_editor.on("change", function(e){ @@ -538,7 +536,7 @@ $('#wizp2_body').append(''); createTable("lidsh", "lidsb", "hue_ids_t"); $('.lidsh').append(createTableRow([$.i18n('edt_dev_spec_lightid_title'),$.i18n('wiz_hue_pos'),$.i18n('wiz_hue_ident')], true)); - $('#wizp2_footer').html(''); + $('#wizp2_footer').html(''); $('#wizp3_body').html(''+$.i18n('wiz_hue_press_link')+'


'); //open modal @@ -751,7 +749,7 @@ serverConfig.smoothing.enable = false; requestWriteConfig(serverConfig, true); - setTimeout(initRestart,200); + resetWizard(); }); $('#btn_wiz_abort').off().on('click', resetWizard); diff --git a/assets/webconfig/server_scripts/demo.py b/assets/webconfig/server_scripts/demo.py deleted file mode 100644 index a7d90ed8..00000000 --- a/assets/webconfig/server_scripts/demo.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python - -print ("hello world"); - - diff --git a/bin/scripts/docker-compile.sh b/bin/scripts/docker-compile.sh new file mode 100644 index 00000000..6d4215b8 --- /dev/null +++ b/bin/scripts/docker-compile.sh @@ -0,0 +1,112 @@ +#!/bin/bash -e + +DOCKER="docker" +# Git repo url of Hyperion +GIT_REPO_URL="https://github.com/hyperion-project/hyperion.ng.git" +# cmake build type +BUILD_TYPE="Release" +# the image tag at hyperionorg/hyperion-ci +BUILD_TARGET="ubuntu1604" +# build packages (.deb .zip ...) +BUILD_PACKAGES=true +# packages string inserted to cmake cmd +PACKAGES="" + +# get current path to this script, independent of calling +pushd . > /dev/null +SCRIPT_PATH="${BASH_SOURCE[0]}" +if ([ -h "${SCRIPT_PATH}" ]); then + while([ -h "${SCRIPT_PATH}" ]); do cd `dirname "$SCRIPT_PATH"`; + SCRIPT_PATH=`readlink "${SCRIPT_PATH}"`; done +fi +cd `dirname ${SCRIPT_PATH}` > /dev/null +SCRIPT_PATH=`pwd`; +popd > /dev/null + +set +e +$DOCKER ps >/dev/null 2>&1 +if [ $? != 0 ]; then + DOCKER="sudo docker" +fi +# check if docker is available +if ! $DOCKER ps >/dev/null; then + echo "Error connecting to docker:" + $DOCKER ps + printHelp + exit 1 +fi +set -e + +# help print function +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 Ubuntu 16.04 (x64) or higher. +## Supports Raspberry Pi (armv6) cross compilation (Raspbian Stretch) +## +## Homepage: https://www.hyperion-project.org +## Forum: https://forum.hyperion-project.org +######################################################## +# These are possible arguments to modify the script behaviour with their default values +# +# docker-compile.sh -h # Show this help message +# docker-compile.sh -t ubuntu1604 # The docker tag, one of ubuntu1604 | cross-qemu-rpistretch +# docker-compile.sh -b Release # cmake Release or Debug build +# docker-compile.sh -p true # If true build packages with CPack +# More informations to docker tags at: https://hub.docker.com/r/hyperionorg/hyperion-ci/" +} + +while getopts t:b:p:h option +do + case "${option}" + in + t) BUILD_TARGET=${OPTARG};; + b) BUILD_TYPE=${OPTARG};; + p) BUILD_PACKAGES=${OPTARG};; + h) printHelp; exit 0;; + esac +done + +# determine package creation +if [ $BUILD_PACKAGES == "true" ]; then + PACKAGES="package" +fi + +echo "---> Initilize with BUILD_TARGET=${BUILD_TARGET}, BUILD_TYPE=${BUILD_TYPE}, BUILD_PACKAGES=${BUILD_PACKAGES}" + +# cleanup deploy folder, create folder for ownership +sudo rm -fr $SCRIPT_PATH/deploy >/dev/null 2>&1 +mkdir $SCRIPT_PATH/deploy >/dev/null 2>&1 + +# get Hyperion source, cleanup previous folder +echo "---> Downloading Hyperion source code from ${GIT_REPO_URL}" +sudo rm -fr $SCRIPT_PATH/hyperion >/dev/null 2>&1 +git clone --recursive --depth 1 -q $GIT_REPO_URL $SCRIPT_PATH/hyperion || { echo "---> Failed to download Hyperion source code! Abort"; exit 1; } + +# start compilation +# Remove container after stop +# Mount /deploy to /deploy +# Mount source dir to /source +# Target docker image +# execute inside container all commands on bash +echo "---> Startup docker..." +$DOCKER run --rm \ + -v "${SCRIPT_PATH}/deploy:/deploy" \ + -v "${SCRIPT_PATH}/hyperion:/source:ro" \ + hyperionorg/hyperion-ci:$BUILD_TARGET \ + /bin/bash -c "mkdir build && cp -r /source/. /build && + cd /build && mkdir build && cd build && + cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} .. || exit 2 && + make -j $(nproc) ${PACKAGES} || exit 3 && + echo '---> Copy binaries and packages to host folder: ${SCRIPT_PATH}/deploy' && + cp -v /build/build/bin/h* /deploy/ 2>/dev/null || : && + cp -v /build/build/Hyperion-* /deploy/ 2>/dev/null || : && + exit 0; + exit 1 " || { echo "---> Hyperion compilation failed! Abort"; exit 4; } + +# overwrite file owner to current user +sudo chown -fR $(stat -c "%U:%G" $SCRIPT_PATH/deploy) $SCRIPT_PATH/deploy + +echo "---> Script finished, view folder ${SCRIPT_PATH}/deploy for compiled packages and binaries" +exit 0 diff --git a/bin/scripts/setup_hyperion_forward.sh b/bin/scripts/setup_hyperion_forward.sh deleted file mode 100644 index 4fe45218..00000000 --- a/bin/scripts/setup_hyperion_forward.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/bash -# Script to add a second or more hyperion instance(s) to the corresponding system service - -# Make sure /sbin is on the path (for service to find sub scripts) -PATH="/sbin:$PATH" - -#Check, if script is running as root -if [ $(id -u) != 0 ]; then - echo '---> Critical Error: Please run the script as root (sudo sh ./setup_hyperion_forward.sh) -> abort' - exit 1 -fi - -#Welcome message -echo '*******************************************************************************' -echo 'This setup script will duplicate the hyperion service' -echo 'Choose the name(s) for one or more config files - one service for each config' -echo 'Created by brindosch - hyperion-project.org - the official Hyperion source.' -echo '*******************************************************************************' - -#Prompt for confirmation to proceed -while true -do -echo -n "---> Do you really want to proceed? (y or n) :" -read CONFIRM -case $CONFIRM in -y|Y|YES|yes|Yes) break ;; -n|N|no|NO|No) -echo "---> Aborting - you entered \"$CONFIRM\"" -exit -;; -*) echo "-> Please enter only y or n" -esac -done -echo "---> You entered \"$CONFIRM\". We will proceed!" -echo "" - -#Check which system we are on -OS_OPENELEC=`grep -m1 -c 'OpenELEC\|RasPlex\|LibreELEC' /etc/issue` -USE_SYSTEMD=`grep -m1 -c systemd /proc/1/comm` -USE_INITCTL=`which /sbin/initctl | wc -l` -USE_SERVICE=`which /usr/sbin/service | wc -l` - -#Setting up the paths to service files -if [ $USE_INITCTL -eq 1 ]; then - SERVICEPATH=/etc/init -elif [ $OS_OPENELEC -eq 1 ]; then - SERVICEPATH=/storage/.config -elif [ $USE_SYSTEMD -eq 1 ]; then - SERVICEPATH=/etc/systemd/system -elif [ $USE_SERVICE -eq 1 ]; then - SERVICEPATH/etc/init.d -fi - -#Setting up the default PROTO/JSON ports -JSONPORT=19444 -PROTOPORT=19445 -# and service count -SERVICEC=1 - -#Setting up the paths to config files -if [ $OS_OPENELEC -eq 1 ]; then - CONFIGPATH=/storage/.config -else CONFIGPATH=/opt/hyperion/config -fi - -#Ask the user for some informations regarding the setup -echo "---> Please enter the config name(s) you want to create" -echo "---> Information: One name creates one service and two names two services etc" -echo '---> Please enter them seperated with a space in a one line row!' -echo '---> example: hyperion.philipshue_1.json hyperion.AtmoOrb_2.json hypthreeconf.json' -echo '---> In any case, add ".json" at the end of each file name' -read -p 'Config file name(s): ' FILENAMES -echo '---> Thank you, we will modify your Hyperion installation now' -sleep 2 - -#Processing input -set $FILENAMES -FWCOUNT=${#} - -#Convert all old config file paths to make sure this script is working (default for new installs with 1.02.0 and higher) -if [ $USE_INITCTL -eq 1 ]; then - sed -i "s|/etc/hyperion.config.json|/etc/hyperion/hyperion.config.json|g" $SERVICEPATH/hyperion.conf -elif [ $OS_OPENELEC -eq 1 ]; then - sleep 0 -elif [ $USE_SYSTEMD -eq 1 ]; then - sed -i "s|/etc/hyperion.config.json|/etc/hyperion/hyperion.config.json|g" $SERVICEPATH/hyperion.service -elif [ $USE_SERVICE -eq 1 ]; then - sed -i "s|/etc/hyperion.config.json|/etc/hyperion/hyperion.config.json|g" $SERVICEPATH/hyperion -fi - -#Processing service files -if [ $USE_INITCTL -eq 1 ]; then - echo "---> Initctl detected, processing service files" - while [ $SERVICEC -le $FWCOUNT ]; do - echo "Processing service ${SERVICEC}: \"hyperion_fw${SERVICEC}.conf\"" - if [ -e "${SERVICEPATH}/hyperion_fw${SERVICEC}.conf" ]; then - echo "Service was already created - skipped" - echo "Input \"${1}\" was skipped" - else - echo "Create ${SERVICEPATH}/hyperion_fw${SERVICEC}.conf" - cp -s $SERVICEPATH/hyperion.conf $SERVICEPATH/hyperion_fw$SERVICEC.conf - echo "Config name changed to \"${1}\" inside \"hyperion_fw${SERVICEC}.conf\"" - sed -i "s/hyperion.config.json/$1/g" $SERVICEPATH/hyperion_fw$SERVICEC.conf - initctl reload-configuration - fi - shift - SERVICEC=$((SERVICEC + 1)) - done -elif [ $OS_OPENELEC -eq 1 ]; then - echo "---> OE/LE detected, processing autostart.sh" - while [ $SERVICEC -le $FWCOUNT ]; do - echo "${SERVICEC}. processing OE autostart.sh entry \"${1}\"" - OE=`grep -m1 -c ${1} $SERVICEPATH/autostart.sh` - if [ $OE -eq 0 ]; then - echo "Add config name \"${1}\" to \"autostart.sh\"" - echo "/storage/hyperion/bin/hyperiond.sh /storage/.config/${1} > /storage/logfiles/hyperion_fw${SERVICEC}.log 2>&1 &" >> /storage/.config/autostart.sh - else - echo "\"${1}\" was already added - skipped" - fi - shift - SERVICEC=$((SERVICEC + 1)) - done -elif [ $USE_SYSTEMD -eq 1 ]; then - echo "---> Systemd detected, processing service files" - while [ $SERVICEC -le $FWCOUNT ]; do - echo "Processing service ${SERVICEC}: \"hyperion_fw${SERVICEC}.service\"" - if [ -e "${SERVICEPATH}/hyperion_fw${SERVICEC}.service" ]; then - echo "Service was already created - skipped" - echo "Input \"${1}\" was skipped" - else - echo "Create ${SERVICEPATH}/hyperion_fw${SERVICEC}.service" - cp -s $SERVICEPATH/hyperion.service $SERVICEPATH/hyperion_fw$SERVICEC.service - echo "Config name changed to \"${1}\" inside \"hyperion_fw${SERVICEC}.service\"" - sed -i "s/hyperion.config.json/$1/g" $SERVICEPATH/hyperion_fw$SERVICEC.service - systemctl -q enable hyperion_fw$SERVICEC.service - fi - shift - SERVICEC=$((SERVICEC + 1)) - done -elif [ $USE_SERVICE -eq 1 ]; then - echo "---> Init.d detected, processing service files" - while [ $SERVICEC -le $FWCOUNT ]; do - echo "Processing service ${SERVICEC}: \"hyperion_fw${SERVICEC}\"" - if [ -e "${SERVICEPATH}/hyperion_fw${SERVICEC}" ]; then - echo "Service was already created - skipped" - echo "Input \"${1}\" was skipped" - else - echo "Create ${SERVICEPATH}/hyperion_fw${SERVICEC}" - cp -s $SERVICEPATH/hyperion $SERVICEPATH/hyperion_fw$SERVICEC - echo "Config name changed to \"${1}\" inside \"hyperion_fw${SERVICEC}\"" - sed -i "s/hyperion.config.json/$1/g" $SERVICEPATH/hyperion_fw$SERVICEC - update-rc.d hyperion_fw$SERVICEC defaults 98 02 - fi - shift - SERVICEC=$((SERVICEC + 1)) - done -fi - -#Service creation done -echo '*******************************************************************************' -echo 'Script done all actions - all input processed' -echo 'Now upload your configuration(s) with HyperCon at the SSH Tab' -echo 'All created Hyperion services will start with your chosen confignames' -echo 'Wiki: wiki.hyperion-project.org Webpage: www.hyperion-project.org' -echo '*******************************************************************************' \ No newline at end of file diff --git a/bin/service/hyperion.systemd b/bin/service/hyperion.systemd index 8d9f1f2c..35817d1b 100644 --- a/bin/service/hyperion.systemd +++ b/bin/service/hyperion.systemd @@ -1,13 +1,14 @@ [Unit] -Description=Hyperion ambient light systemd service +Description=Hyperion ambient light systemd service for user %i After=network.target [Service] ExecStart=/usr/bin/hyperiond WorkingDirectory=/usr/share/hyperion/bin +User=%i TimeoutStopSec=5 KillMode=mixed -Restart=always +Restart=on-failure RestartSec=2 [Install] diff --git a/bin/service/hyperion.systemd.RO b/bin/service/hyperion.systemd.RO index b763a107..179871c6 100644 --- a/bin/service/hyperion.systemd.RO +++ b/bin/service/hyperion.systemd.RO @@ -9,7 +9,7 @@ Environment=LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. ExecStart=./hyperiond /storage/.config/hyperion/hyperion.config.json TimeoutStopSec=5 KillMode=mixed -Restart=always +Restart=on-failure RestartSec=2 [Install] diff --git a/cmake/FindDebBuilder.cmake b/cmake/FindDebBuilder.cmake new file mode 100644 index 00000000..e2f2c9ac --- /dev/null +++ b/cmake/FindDebBuilder.cmake @@ -0,0 +1,23 @@ +# - Find package for .deb building +# Find the .deb building executable and extract the version number +# +# OUTPUT Variables +# +# DEB_BUILDER_FOUND +# True if the deb builder package was found +# DEB_BUILDER_EXECUTABLE +# The deb builder executable location +# DEB_BUILDER_VERSION +# A string denoting the version of deb builder that has been found + +find_program ( DEB_BUILDER_EXECUTABLE dpkg-deb ) + +if ( DEB_BUILDER_EXECUTABLE ) + SET( DEB_BUILDER_FOUND TRUE ) + execute_process ( COMMAND ${DEB_BUILDER_EXECUTABLE} --version OUTPUT_VARIABLE DEB_VERSION_RAW ERROR_QUIET ) + if (DEB_VERSION_RAW) + string ( REGEX REPLACE "^RPM-Version ([0-9]+.[0-9]+.[0-9]+),.*" "\\1" DEB_BUILDER_VERSION ${DEB_VERSION_RAW}) + else () + set ( DEB_BUILDER_VERSION "unknown" ) + endif() +endif () diff --git a/cmake/FindGitVersion.cmake b/cmake/FindGitVersion.cmake index bbcdfb79..b48f70ca 100644 --- a/cmake/FindGitVersion.cmake +++ b/cmake/FindGitVersion.cmake @@ -1,9 +1,10 @@ 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 ) STRING ( STRIP "${BUILD_ID}" BUILD_ID ) STRING ( STRIP "${VERSION_ID}" VERSION_ID ) -SET ( HYPERION_BUILD_ID "${VERSION_ID} (${BUILD_ID})" ) +STRING ( STRIP "${GIT_REMOTE_PATH}" GIT_REMOTE_PATH ) +SET ( HYPERION_BUILD_ID "${VERSION_ID} (${BUILD_ID}) Git Remote: ${GIT_REMOTE_PATH}" ) message ( STATUS "Current Version: ${HYPERION_BUILD_ID}" ) - diff --git a/cmake/FindRpmBuilder.cmake b/cmake/FindRpmBuilder.cmake new file mode 100644 index 00000000..07a22468 --- /dev/null +++ b/cmake/FindRpmBuilder.cmake @@ -0,0 +1,23 @@ +# - Find package for .rpm building +# Find the .rpm building executable and extract the version number +# +# OUTPUT Variables +# +# RPM_BUILDER_FOUND +# True if the rpm package was found +# RPM_BUILDER_EXECUTABLE +# The rpm executable location +# RPM_BUILDER_VERSION +# A string denoting the version of rpm that has been found + +find_program ( RPM_BUILDER_EXECUTABLE rpm ) + +if ( RPM_BUILDER_EXECUTABLE ) + SET( RPM_BUILDER_FOUND TRUE ) + execute_process ( COMMAND ${RPM_BUILDER_EXECUTABLE} --version OUTPUT_VARIABLE RPM_VERSION_RAW ERROR_QUIET ) + if (RPM_VERSION_RAW) + string ( REGEX REPLACE "^RPM-Version ([0-9]+.[0-9]+.[0-9]+),.*" "\\1" RPM_BUILDER_VERSION ${RPM_VERSION_RAW}) + else () + set ( RPM_BUILDER_VERSION "unknown" ) + endif() +endif () diff --git a/cmake/debian/postinst b/cmake/debian/postinst index a49b4795..eb9fa7ec 100644 --- a/cmake/debian/postinst +++ b/cmake/debian/postinst @@ -6,6 +6,7 @@ install_file() dest="$2" if [ ! -e "$dest" ] + then cp "$src" "${dest}" return 1 else @@ -15,10 +16,7 @@ install_file() } -echo "--- hyperion ambient light postinstall ---" -echo "- install configuration template" -mkdir -p /etc/hyperion -mkdir -p /usr/share/hyperion/custom-effects +echo "---Hyperion ambient light postinstall ---" #check system CPU_RPI=`grep -m1 -c 'BCM2708\|BCM2709\|BCM2710\|BCM2835' /proc/cpuinfo` @@ -27,42 +25,54 @@ 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 + add default port -address=$(ip -o -4 a | awk '$2 == "eth0" { gsub(/\/.*/, "", $4); print $4 }')":8099" +#get current system ip +NET_IP=`hostname -I | cut -d " " -f1` -#check if hyperion is running -HYPERION_RUNNING=false -pgrep hyperiond > /dev/null 2>&1 && HYPERION_RUNNING=true +# search for users in system, returns first entry +FOUND_USR=`who | grep -o '^\w*\b'` || "root" + +# determine if we should use a service +ENABLE_SERVICE=0 +STARTUP_MSG="echo ---> You can start Hyperion from your menu now" + +if [ $CPU_RPI -eq 1 ]; then + ENABLE_SERVICE=1 + STARTUP_MSG="echo ---> Hyperion has been installed as service, it will start on each system startup" +fi start_msg="" restart_msg="" -SERVICE_POSTFIX="" if grep -m1 systemd /proc/1/comm > /dev/null then - echo "--> init deamon: systemd" + echo "---> init deamon: systemd" # systemd - $HYPERION_RUNNING && systemctl stop hyperiond 2> /dev/null - install_file /usr/share/hyperion/service/hyperion.systemd /etc/systemd/system/hyperiond.service && systemctl -q enable hyperiond.service - start_msg="--> systemctl start hyperiond" - systemctl start hyperiond + install_file /usr/share/hyperion/service/hyperion.systemd /etc/systemd/system/hyperiond@.service + # service registration just on Raspberry Pi, probably need to ask the user how we should use the service. TODO service start in user login scope eg for x11?! + if [ $ENABLE_SERVICE -eq 1 ]; then + systemctl enable hyperiond"@${FOUND_USR}".service + start_msg="--> systemctl start hyperiond for user ${FOUND_USR}" + systemctl start hyperiond"@${FOUND_USR}" + fi elif [ -e /sbin/initctl ] then - echo "--> init deamon: upstart" + echo "---> init deamon: upstart" # upstart - $HYPERION_RUNNING && initctl stop hyperiond - install_file /usr/share/hyperion/service/hyperiond.initctl /etc/init/hyperion.conf && initctl reload-configuration - start_msg="--> initctl start hyperiond" - initctl start hyperiond + if [ $ENABLE_SERVICE -eq 1 ]; then + install_file /usr/share/hyperion/service/hyperiond.initctl /etc/init/hyperion.conf && initctl reload-configuration + start_msg="--> initctl start hyperiond" + initctl start hyperiond + fi else - echo "--> init deamon: sysV" + echo "---> init deamon: sysV" # sysV - $HYPERION_RUNNING && service hyperiond stop 2>/dev/null - install_file /usr/share/hyperion/service/hyperion.init /etc/init.d/hyperiond && chmod +x /etc/init.d/hyperiond && update-rc.d hyperiond defaults 98 02 - start_msg="--> service hyperiond start" - service hyperiond start + if [ $ENABLE_SERVICE -eq 1 ]; then + install_file /usr/share/hyperion/service/hyperion.init /etc/init.d/hyperiond && chmod +x /etc/init.d/hyperiond && update-rc.d hyperiond defaults 98 02 + start_msg="---> service hyperiond start" + service hyperiond start + fi fi #cleanup @@ -79,7 +89,16 @@ ln -fs $BINSP/hyperion-framebuffer $BINTP/hyperion-framebuffer 2>/dev/null ln -fs $BINSP/hyperion-dispmanx $BINTP/hyperion-dispmanx 2>/dev/null ln -fs $BINSP/hyperion-x11 $BINTP/hyperion-x11 2>/dev/null ln -fs $BINSP/hyperion-aml $BINTP/hyperion-aml 2>/dev/null +ln -fs $BINSP/hyperion-qt $BINTP/hyperion-qt 2>/dev/null +# install desktop icons +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 + +# cleanup desktop icons +rm -r /usr/share/hyperion/desktop 2>/dev/null #Check, if dtparam=spi=on is in place if [ $CPU_RPI -eq 1 ]; then @@ -100,9 +119,14 @@ if [ $CPU_RPI -eq 1 ]; then fi fi +echo ${start_msg} + echo "-----------------------------------------------------------------------------" -echo "--> Hyperion has been installed/updated!" -echo "--> For configuration, visit with your browser: ${address}" +echo "---> Hyperion has been installed/updated!" +echo "---> " +$STARTUP_MSG +echo "---> For configuration, visit with your browser: ${NET_IP}:8090" +echo "---> or if already used by another service try: ${NET_IP}:8091" $REBOOTMESSAGE echo "-----------------------------------------------------------------------------" echo "Webpage: www.hyperion-project.org" @@ -110,28 +134,14 @@ echo "Wiki: wiki.hyperion-project.org" echo "Forum: forum.hyperion-project.org" echo "-----------------------------------------------------------------------------" -# try to open the browser for desktops. TODO: add headless detection(?) -if [ $CPU_X32X64 -eq 1] -echo "--> Will open browser with target: ${address}" - if [[ -e /usr/bin/xdg-open ]] - then - xdg-open http://"$address" - elif [[ -e /usr/bin/x-www-browser ]] - then - x-www-browser http://"$address" - elif [[ -e /usr/bin/www-browser ]] - then - www-browser http://"$address" - fi -fi - if [ -e /opt/hyperion/ ] then echo echo "---------------------------------------------------------------------------------" - echo "- It seemd that you have an older version of hyperion installed in /opt/hyerion -" - echo "- please remove it and check your config to avoid problems -" + echo "- It seemd that you have an older version of hyperion installed in /opt/hyperion -" + echo "- please remove it to avoid problems -" echo "---------------------------------------------------------------------------------" fi +exit 0 diff --git a/cmake/debian/preinst b/cmake/debian/preinst index 335aa696..0f41fb6c 100644 --- a/cmake/debian/preinst +++ b/cmake/debian/preinst @@ -1,51 +1,61 @@ #!/bin/sh -# check which init script we should use -USE_SYSTEMD=`grep -m1 -c systemd /proc/1/comm` -USE_INITCTL=`which /sbin/initctl | wc -l` -USE_SERVICE=`which /usr/sbin/service | wc -l` +echo "---Hyperion ambient light preinst ---" -#check for hyperion install -if [ -d /usr/share/hyperion/bin ];then - if [ -e /etc/hyperion/hyperion.config.json ];then - file=`grep -m1 -c '"general"' /etc/hyperion/hyperion.config.json` - if [ $file -ne 1 ]; then - echo "--> It seems you are running an old version of Hyperion (1.X). Will create a backup at /usr/share/hyperion/Backup_Hyperion_1.0 and reset configuration / system service" - - # Stop hyperion daemon if it is running - echo '---> Stop Hyperion, if necessary' - if [ $USE_SYSTEMD -eq 1 ]; then - service hyperion stop 2>/dev/null - elif [ $USE_INITCTL -eq 1 ]; then - /sbin/initctl stop hyperion 2>/dev/null - elif [ $USE_SERVICE -eq 1 ]; then - /usr/sbin/service hyperion stop 2>/dev/null - fi - - #Backup - echo "--> Move old config(s) and files to /usr/share/hyperion/Backup_Hyperion_1.0" - mkdir /usr/share/hyperion/Backup_Hyperion_1.0 - mv /usr/share/hyperion /usr/share/hyperion/Backup_Hyperion_1.0 - mv /etc/hyperion/* /usr/share/hyperion/Backup_Hyperion_1.0 +# search for users in system, returns first entry +FOUND_USR=`who | grep -o '^\w*\b'` || "root" - #Disabling and delete service files - if [ $USE_SYSTEMD -eq 1 ]; then - # Delete and disable Hyperion systemd script - echo '---> Delete and disable Hyperion systemd service' - systemctl disable hyperion.service - rm -v /etc/systemd/system/hyperion* 2>/dev/null - elif [ $USE_INITCTL -eq 1 ]; then - echo '---> Delete and disable Hyperion initctl script' - rm -v /etc/init/hyperion* 2>/dev/null - initctl reload-configuration - elif [ $USE_SERVICE -eq 1 ]; then - # Delete and disable Hyperion init.d script - echo '---> Delete and disable Hyperion init.d script' - update-rc.d -f hyperion remove - rm /etc/init.d/hyperion* 2>/dev/null - fi - - echo "--> Hyperion 1.0 installation has been moved" - fi +# stop running daemon before we install +if pgrep hyperiond > /dev/null 2>&1 +then + if grep -m1 systemd /proc/1/comm > /dev/null + then + echo "--> stop init deamon: systemd" + # systemd + systemctl stop hyperiond"@${FOUND_USR}" 2> /dev/null + + elif [ -e /sbin/initctl ] + then + echo "--> stop init deamon: upstart" + # upstart + initctl stop hyperiond + + else + echo "--> stop init deamon: sysV" + # sysV + service hyperiond stop 2>/dev/null fi fi + +# In case we don't use a service kill all instances +killall hyperiond 2> /dev/null + +# overwrite last return code +exit 0 + +#$USR=hyperionIS; + +#addToGroup() +##{ +# getent group $1 && adduser $USR $1; +#} + +#check if user exists +#if id $USR >/dev/null 2>&1; then +# echo "--> hyperion user exists, skip creation"; +#else + ## create user +# echo "--> Create Hyperion user"; +# adduser --system --group $USR; +#fi + +# add user to groups if required +## secondary user groups that are required to access system things +#addToGroup(dialout); +#addToGroup(video); +#addToGroup(audio); +#addToGroup(systemd-journal); +# platform specific groups +#addToGroup(i2c); +#addToGroup(spi); +#addToGroup(gpio); diff --git a/cmake/debian/prerm b/cmake/debian/prerm new file mode 100644 index 00000000..388c79ae --- /dev/null +++ b/cmake/debian/prerm @@ -0,0 +1,50 @@ +#!/bin/sh + +echo "---Hyperion ambient light prerm ---" + +# search for users in system, returns first entry +FOUND_USR=`who | grep -o '^\w*\b'` || "root" + +# stop running daemon before we delete it +HYPERION_RUNNING=false +pgrep hyperiond > /dev/null 2>&1 && HYPERION_RUNNING=true + +if grep -m1 systemd /proc/1/comm > /dev/null +then + echo "---> stop init deamon: systemd" + # systemd + $HYPERION_RUNNING && systemctl stop hyperiond"@${FOUND_USR}" 2> /dev/null + # disable user specific symlink + echo "---> Disable service and remove entry" + systemctl -q disable hyperiond"@${FOUND_USR}" + rm -v /etc/systemd/system/hyperiond@.service 2>/dev/null + +elif [ -e /sbin/initctl ] +then + echo "---> stop init deamon: upstart" + # upstart + $HYPERION_RUNNING && initctl stop hyperiond + echo "---> Remove upstart service" + rm -v /etc/init/hyperion* 2>/dev/null + initctl reload-configuration + +else + echo "---> stop init deamon: sysV" + # sysV + $HYPERION_RUNNING && service hyperiond stop 2>/dev/null + echo "---> Remove sysV service" + update-rc.d -f hyperion remove + rm /etc/init.d/hyperion* 2>/dev/null +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 +rm -v /usr/share/applications/hyperion* 2>/dev/null +rm -rv /usr/share/pixmaps/hyperion 2>/dev/null + +exit 0 diff --git a/cmake/desktop/hyperiond.desktop b/cmake/desktop/hyperiond.desktop new file mode 100644 index 00000000..d47a38d3 --- /dev/null +++ b/cmake/desktop/hyperiond.desktop @@ -0,0 +1,11 @@ +[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 +Terminal=false +TryExec=hyperiond +Exec=hyperiond +Type=Application +StartupNotify=false +Categories=Application; diff --git a/cmake/desktop/hyperiond_128.png b/cmake/desktop/hyperiond_128.png new file mode 100644 index 00000000..8da3dd17 Binary files /dev/null and b/cmake/desktop/hyperiond_128.png differ diff --git a/cmake/macos/Hyperion.icns b/cmake/osxbundle/Hyperion.icns similarity index 100% rename from cmake/macos/Hyperion.icns rename to cmake/osxbundle/Hyperion.icns diff --git a/cmake/macos/Info.plist b/cmake/osxbundle/Info.plist similarity index 100% rename from cmake/macos/Info.plist rename to cmake/osxbundle/Info.plist diff --git a/cmake/osxbundle/launch.sh b/cmake/osxbundle/launch.sh new file mode 100644 index 00000000..0c3ea053 --- /dev/null +++ b/cmake/osxbundle/launch.sh @@ -0,0 +1,5 @@ +#!/bin/sh +cd "$(dirname "$0")" +# Path to hyperiond!? +cd ../Resources/bin +exec ./hyperiond "$@" diff --git a/cmake/packages.cmake b/cmake/packages.cmake index 7fef5d09..5e9765b3 100644 --- a/cmake/packages.cmake +++ b/cmake/packages.cmake @@ -1,38 +1,80 @@ # cmake file for generating distribution packages +# default packages to build IF (APPLE) - SET ( CPACK_GENERATOR "TGZ" "Bundle") # "RPM" -ELSE() - SET ( CPACK_GENERATOR "DEB" "TGZ" "STGZ") # "RPM" + SET ( CPACK_GENERATOR "TGZ" "Bundle") +ELSEIF (UNIX) + SET ( CPACK_GENERATOR "TGZ" "STGZ") +ELSEIF (WIN32) + SET ( CPACK_GENERATOR "ZIP") ENDIF() -SET ( CPACK_PACKAGE_NAME "hyperion" ) +# Determine packages by found generator executables +find_package(RpmBuilder) +find_package(DebBuilder) +IF(RPM_BUILDER_FOUND) + message("CPACK: Found RPM builder") + SET ( CPACK_GENERATOR ${CPACK_GENERATOR} "RPM") +ENDIF() +IF(DEB_BUILDER_FOUND) + message("CPACK: Found DEB builder") + SET ( CPACK_GENERATOR ${CPACK_GENERATOR} "DEB") +ENDIF() + +# Apply to all packages, some of these can be overwritten with generator specific content +# https://cmake.org/cmake/help/v3.5/module/CPack.html + +SET ( CPACK_PACKAGE_NAME "Hyperion" ) SET ( CPACK_PACKAGE_DESCRIPTION_SUMMARY "Hyperion is an open source ambient light implementation" ) SET ( CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README.md" ) +SET ( CPACK_PACKAGE_FILE_NAME "Hyperion-${HYPERION_VERSION_MAJOR}.${HYPERION_VERSION_MINOR}.${HYPERION_VERSION_PATCH}-${CMAKE_SYSTEM_NAME}") +SET ( CPACK_PACKAGE_CONTACT "packages@hyperion-project.org") +SET ( CPACK_PACKAGE_EXECUTABLES "hyperiond;Hyperion" ) +SET ( CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/icons/hyperion-icon-32px.png") +SET ( CPACK_PACKAGE_VERSION_MAJOR "${HYPERION_VERSION_MAJOR}") +SET ( CPACK_PACKAGE_VERSION_MINOR "${HYPERION_VERSION_MINOR}") +SET ( CPACK_PACKAGE_VERSION_PATCH "${HYPERION_VERSION_PATCH}") SET ( CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" ) +SET ( CPACK_CREATE_DESKTOP_LINKS "hyperiond;Hyperion" ) -SET ( CPACK_DEBIAN_PACKAGE_MAINTAINER "Hyperion Team") -SET ( CPACK_DEBIAN_PACKAGE_NAME "Hyperion" ) -SET ( CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/preinst" ) -SET ( CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://www.hyperion-project.org" ) -SET ( CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5core5a (>= 5.2.0), libqt5network5 (>= 5.2.0), libqt5gui5 (>= 5.2.0), libqt5serialport5 (>= 5.2.0), libavahi-core7 (>= 0.6.31), libavahi-compat-libdnssd1 (>= 0.6.31), libusb-1.0-0, libpython3.4, libc6" ) + +# Specific CPack Package Generators +# https://cmake.org/Wiki/CMake:CPackPackageGenerators +# .deb files for apt + +SET ( CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/preinst;${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/prerm" ) +SET ( CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5core5a (>= 5.5.0), libqt5network5 (>= 5.5.0), libqt5gui5 (>= 5.5.0), libqt5serialport5 (>= 5.5.0), libqt5sql5 (>= 5.5.0), libqt5sql5-sqlite (>= 5.5.0), libavahi-core7 (>= 0.6.31), libavahi-compat-libdnssd1 (>= 0.6.31), libusb-1.0-0, libpython3.5, libc6" ) SET ( CPACK_DEBIAN_PACKAGE_SECTION "Miscellaneous" ) -SET ( CPACK_RPM_PACKAGE_NAME "Hyperion" ) -SET ( CPACK_RPM_PACKAGE_URL "https://github.com/hyperion-project/hyperion.ng" ) +# .rpm for rpm +# https://cmake.org/cmake/help/v3.5/module/CPackRPM.html +SET ( CPACK_RPM_PACKAGE_RELEASE 1) +SET ( CPACK_RPM_PACKAGE_LICENSE "MIT") +SET ( CPACK_RPM_PACKAGE_GROUP "Applications") +SET ( CPACK_RPM_PACKAGE_REQUIRES "qt5-qtbase >= 5.5.0, qt5-qtbase-gui >= 5.5.0, qt5-qtserialport >= 5.5.0, avahi-libs >= 0.6.31, avahi-compat-libdns_sd >= 0.6.31, libusbx, python35 >= 3.5.0") +# Notes: This is a dependency list for Fedora 27, different .rpm OSes use different names for their deps +SET ( CPACK_RPM_PRE_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/rpm/preinst" ) SET ( CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/rpm/postinst" ) +SET ( CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/rpm/prerm" ) -SET ( CPACK_PACKAGE_FILE_NAME "Hyperion") -SET ( CPACK_PACKAGE_ICON ${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/Hyperion.icns ) +# OSX "Bundle" generator TODO Add more osx generators +# https://cmake.org/cmake/help/v3.10/module/CPackBundle.html SET ( CPACK_BUNDLE_NAME "Hyperion" ) -SET ( CPACK_BUNDLE_ICON ${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/Hyperion.icns ) -SET ( CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/Info.plist ) -#SET ( CPACK_BUNDLE_STARTUP_COMMAND - path to a file that will be executed when the user opens the bundle. Could be a shell-script or a binary. ) +SET ( CPACK_BUNDLE_ICON ${CMAKE_CURRENT_SOURCE_DIR}/cmake/osxbundle/Hyperion.icns ) +SET ( CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/cmake/osxbundle/Info.plist ) +SET ( CPACK_BUNDLE_STARTUP_COMMAND "${CMAKE_SOURCE_DIR}/cmake/osxbundle/launch.sh" ) -SET(CPACK_PACKAGE_VERSION_MAJOR "${HYPERION_VERSION_MAJOR}") -SET(CPACK_PACKAGE_VERSION_MINOR "${HYPERION_VERSION_MINOR}") -SET(CPACK_PACKAGE_VERSION_PATCH "${HYPERION_VERSION_PATCH}") +# NSIS for windows, requires NSIS TODO finish +SET ( CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cmake/nsis/installer.ico") +SET ( CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/cmake/nsis/uninstaller.ico") +#SET ( CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cmake/nsis/installer.bmp") #bmp required? If so, wrap in WIN32 check else: Use default icon instead +SET ( CPACK_NSIS_MODIFY_PATH ON) +SET ( CPACK_NSIS_DISPLAY_NAME "Hyperion Installer") +SET ( CPACK_NSIS_INSTALLED_ICON_NAME "Link to .exe") +SET ( CPACK_NSIS_HELP_LINK "https://www.hyperion-project.org") +SET ( CPACK_NSIS_URL_INFO_ABOUT "https://www.hyperion-project.org") +# define the install components SET ( CPACK_COMPONENTS_ALL "${PLATFORM}" ) SET ( CPACK_ARCHIVE_COMPONENT_INSTALL ON ) SET ( CPACK_DEB_COMPONENT_INSTALL ON ) diff --git a/cmake/rpm/postinst b/cmake/rpm/postinst index 677ddc66..eb9fa7ec 100644 --- a/cmake/rpm/postinst +++ b/cmake/rpm/postinst @@ -5,53 +5,143 @@ install_file() src="$1" dest="$2" - if [ -e "$dest" ] && ! cmp --quiet "$src" "$dest" + if [ ! -e "$dest" ] then - cp "$src" "${dest}.new" - else cp "$src" "${dest}" + return 1 + else + echo "--> Service file already exists, skip creation" + return 0 fi } -echo "--- hyperion ambilight postinstall ---" -echo "- install configuration template" -mkdir -p /etc/hyperion -install_file /usr/share/hyperion/config/hyperion.config.json /etc/hyperion/hyperion.config.json +echo "---Hyperion ambient light postinstall ---" +#check system +CPU_RPI=`grep -m1 -c 'BCM2708\|BCM2709\|BCM2710\|BCM2835' /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` + +# search for users in system, returns first entry +FOUND_USR=`who | grep -o '^\w*\b'` || "root" + +# determine if we should use a service +ENABLE_SERVICE=0 +STARTUP_MSG="echo ---> You can start Hyperion from your menu now" + +if [ $CPU_RPI -eq 1 ]; then + ENABLE_SERVICE=1 + STARTUP_MSG="echo ---> Hyperion has been installed as service, it will start on each system startup" +fi + +start_msg="" +restart_msg="" if grep -m1 systemd /proc/1/comm > /dev/null then + echo "---> init deamon: systemd" # systemd - echo - systemctl stop hyperion 2> /dev/null - install_file /usr/share/hyperion/service/hyperion.systemd.sh /etc/systemd/system/hyperion.service - systemctl -q enable hyperion.service -# if [ $OS_OSMC -eq 1 ]; then -# echo '---> Modify systemd script for OSMC usage' -# # Wait until kodi is sarted (for kodi checker) -# sed -i '/After = mediacenter.service/d' /etc/systemd/system/hyperion.service -# sed -i '/Unit/a After = mediacenter.service' /etc/systemd/system/hyperion.service -# sed -i 's/User=osmc/User=root/g' /etc/systemd/system/hyperion.service -# sed -i 's/Group=osmc/Group=root/g' /etc/systemd/system/hyperion.service -# systemctl -q daemon-reload -# fi - systemctl start hyperion + install_file /usr/share/hyperion/service/hyperion.systemd /etc/systemd/system/hyperiond@.service + # service registration just on Raspberry Pi, probably need to ask the user how we should use the service. TODO service start in user login scope eg for x11?! + if [ $ENABLE_SERVICE -eq 1 ]; then + systemctl enable hyperiond"@${FOUND_USR}".service + start_msg="--> systemctl start hyperiond for user ${FOUND_USR}" + systemctl start hyperiond"@${FOUND_USR}" + fi elif [ -e /sbin/initctl ] then + echo "---> init deamon: upstart" # upstart - install_file /usr/share/hyperion/service/hyperion.initctl.sh /etc/init/hyperion.conf - initctl reload-configuration - initctl start hyperion + if [ $ENABLE_SERVICE -eq 1 ]; then + install_file /usr/share/hyperion/service/hyperiond.initctl /etc/init/hyperion.conf && initctl reload-configuration + start_msg="--> initctl start hyperiond" + initctl start hyperiond + fi else + echo "---> init deamon: sysV" # sysV - service hyperion stop 2>/dev/null - install_file /usr/share/hyperion/service/hyperion.init.sh /etc/init.d/hyperion - chmod +x /etc/init.d/hyperion - update-rc.d hyperion defaults 98 02 - service hyperion start + if [ $ENABLE_SERVICE -eq 1 ]; then + install_file /usr/share/hyperion/service/hyperion.init /etc/init.d/hyperiond && chmod +x /etc/init.d/hyperiond && update-rc.d hyperiond defaults 98 02 + start_msg="---> service hyperiond start" + service hyperiond start + fi fi -echo "- done" +#cleanup +rm -r /usr/share/hyperion/service + +#link binarys and set exec bit +BINSP=/usr/share/hyperion/bin +BINTP=/usr/bin +chmod +x -R $BINSP +ln -fs $BINSP/hyperiond $BINTP/hyperiond +ln -fs $BINSP/hyperion-remote $BINTP/hyperion-remote +ln -fs $BINSP/hyperion-v4l2 $BINTP/hyperion-v4l2 +ln -fs $BINSP/hyperion-framebuffer $BINTP/hyperion-framebuffer 2>/dev/null +ln -fs $BINSP/hyperion-dispmanx $BINTP/hyperion-dispmanx 2>/dev/null +ln -fs $BINSP/hyperion-x11 $BINTP/hyperion-x11 2>/dev/null +ln -fs $BINSP/hyperion-aml $BINTP/hyperion-aml 2>/dev/null +ln -fs $BINSP/hyperion-qt $BINTP/hyperion-qt 2>/dev/null + +# install desktop icons +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 + +# cleanup desktop icons +rm -r /usr/share/hyperion/desktop 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 "-----------------------------------------------------------------------------" +echo "---> Hyperion has been installed/updated!" +echo "---> " +$STARTUP_MSG +echo "---> For configuration, visit with your browser: ${NET_IP}:8090" +echo "---> or if already used by another service try: ${NET_IP}:8091" +$REBOOTMESSAGE +echo "-----------------------------------------------------------------------------" +echo "Webpage: www.hyperion-project.org" +echo "Wiki: wiki.hyperion-project.org" +echo "Forum: forum.hyperion-project.org" +echo "-----------------------------------------------------------------------------" + + +if [ -e /opt/hyperion/ ] +then + echo + echo "---------------------------------------------------------------------------------" + echo "- It seemd that you have an older version of hyperion installed in /opt/hyperion -" + echo "- please remove it to avoid problems -" + echo "---------------------------------------------------------------------------------" +fi + +exit 0 diff --git a/cmake/rpm/preinst b/cmake/rpm/preinst new file mode 100644 index 00000000..3dbfdd2f --- /dev/null +++ b/cmake/rpm/preinst @@ -0,0 +1,61 @@ +#!/bin/sh + +echo "---Hyperion ambient light preinst ---" + +# search for users in system, returns first entry +FOUND_USR=`who | grep -o '^\w*\b'` || "root" + +# stop running daemon before we install +if pgrep hyperiond > /dev/null 2>&1 +then + if grep -m1 systemd /proc/1/comm > /dev/null + then + echo "--> stop init deamon: systemd" + # systemd + systemctl stop hyperiond"@${FOUND_USR}" 2> /dev/null + + elif [ -e /sbin/initctl ] + then + echo "--> stop init deamon: upstart" + # upstart + initctl stop hyperiond + + else + echo "--> stop init deamon: sysV" + # sysV + service hyperiond stop 2>/dev/null + fi +fi + +# In case we don't use a service kill all instances +killall hyperiond 2> /dev/null + +exit 0 + + +#$USR=hyperionIS; + +#addToGroup() +##{ +# getent group $1 && adduser $USR $1; +#} + +#check if user exists +#if id $USR >/dev/null 2>&1; then +# echo "--> hyperion user exists, skip creation"; +#else + ## create user +# echo "--> Create Hyperion user"; +# adduser --system --group $USR; +#fi + +# add user to groups if required +## secondary user groups that are required to access system things +#addToGroup(dialout); +#addToGroup(video); +#addToGroup(audio); +#addToGroup(systemd-journal); +# platform specific groups +#addToGroup(i2c); +#addToGroup(spi); +#addToGroup(gpio); diff --git a/cmake/rpm/prerm b/cmake/rpm/prerm new file mode 100644 index 00000000..388c79ae --- /dev/null +++ b/cmake/rpm/prerm @@ -0,0 +1,50 @@ +#!/bin/sh + +echo "---Hyperion ambient light prerm ---" + +# search for users in system, returns first entry +FOUND_USR=`who | grep -o '^\w*\b'` || "root" + +# stop running daemon before we delete it +HYPERION_RUNNING=false +pgrep hyperiond > /dev/null 2>&1 && HYPERION_RUNNING=true + +if grep -m1 systemd /proc/1/comm > /dev/null +then + echo "---> stop init deamon: systemd" + # systemd + $HYPERION_RUNNING && systemctl stop hyperiond"@${FOUND_USR}" 2> /dev/null + # disable user specific symlink + echo "---> Disable service and remove entry" + systemctl -q disable hyperiond"@${FOUND_USR}" + rm -v /etc/systemd/system/hyperiond@.service 2>/dev/null + +elif [ -e /sbin/initctl ] +then + echo "---> stop init deamon: upstart" + # upstart + $HYPERION_RUNNING && initctl stop hyperiond + echo "---> Remove upstart service" + rm -v /etc/init/hyperion* 2>/dev/null + initctl reload-configuration + +else + echo "---> stop init deamon: sysV" + # sysV + $HYPERION_RUNNING && service hyperiond stop 2>/dev/null + echo "---> Remove sysV service" + update-rc.d -f hyperion remove + rm /etc/init.d/hyperion* 2>/dev/null +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 +rm -v /usr/share/applications/hyperion* 2>/dev/null +rm -rv /usr/share/pixmaps/hyperion 2>/dev/null + +exit 0 diff --git a/config/hyperion.config.json.commented b/config/hyperion.config.json.commented index 10f1a8ce..65db7807 100644 --- a/config/hyperion.config.json.commented +++ b/config/hyperion.config.json.commented @@ -20,14 +20,14 @@ /// Device configuration contains the following fields: /// * 'name' : The user friendly name of the device (only used for display purposes) - /// * 'type' : The type of the device or leds (known types for now are - /// APA102, WS2801, P9813, LPD6803, LPD8806, ---------PWM---------, WS2812b (just RPi1), WS281X (RPi1, RPi2, RPi3), --------OTHER--------, PhilipsHUE, AtmoOrb, PiBlaster, Tinkerforge, FadeCandy, RawHID (USB), UDP, SEDU, TPM2, USBASP-WS2801, USBASP-WS2812, ------3rd PARTY------, Adalight, AdalightAPA102, Atmo, Lightpack, Multi-Lightpack, Paintpack, Test (file), None) + /// * 'type' : The type of the device /// * [device type specific configuration] /// * 'colorOrder' : The order of the color bytes ('rgb', 'rbg', 'bgr', etc.). /// * 'rewriteTime': in ms. Data is resend to leds, if no new data is available in thistime. 0 means no refresh "device" : { "type" : "file", + "hardwareLedCount" : 1, "output" : "/dev/null", "rate" : 1000000, "colorOrder" : "rgb", @@ -43,7 +43,7 @@ /// * 'id' : The unique identifier of the channel adjustments (eg 'device_1') /// * 'leds' : The indices (or index ranges) of the leds to which this channel adjustment applies /// (eg '0-5, 9, 11, 12-17'). The indices are zero based. - /// * 'black'/'white'/'red'/'green'/'blue'/'cyan'/'magenta'/'yellow' : Array of RGB to adjust the output color + /// * 'white'/'red'/'green'/'blue'/'cyan'/'magenta'/'yellow' : Array of RGB to adjust the output color /// * 'gammaRed'/'gammaGreen'/'gammaBlue' : Gamma value for each channel /// * 'id' : The unique identifier of the channel adjustments (eg 'device_1') /// * 'id' : The unique identifier of the channel adjustments (eg 'device_1') @@ -60,7 +60,6 @@ { "id" : "default", "leds" : "*", - "black" : [0,0,0], "white" : [255,255,255], "red" : [255,0,0], "green" : [0,255,0], @@ -99,15 +98,9 @@ }, /// Configuration for the embedded V4L2 grabber - /// * enable : Enable or disable the v4lgrabber (true/false) - /// * device : V4L2 Device to use [default="/dev/video0"] - /// * input : V4L2 input to use [default=0] - /// * standard : Video standard (PAL/NTSC/SECAM) [default="PAL"] - /// * width : V4L2 width to set [default=-1] - /// * height : V4L2 height to set [default=-1] - /// * frameDecimation : Frame decimation factor [default=2] + /// * device : V4L2 Device to use [default="auto"] (Auto detection) + /// * standard : Video standard (PAL/NTSC/SECAM/NO_CHANGE) [default="NO_CHANGE"] /// * sizeDecimation : Size decimation factor [default=8] - /// * priority : Hyperion priority channel [default=900] /// * cropLeft : Cropping from the left [default=0] /// * cropRight : Cropping from the right [default=0] /// * cropTop : Cropping from the top [default=0] @@ -123,13 +116,8 @@ "grabberV4L2" : [ { - "enable" : false, "device" : "auto", - "input" : 0, - "standard" : "PAL", - "width" : 0, - "height" : 0, - "frameDecimation" : 2, + "standard" : "NO_CHANGE", "sizeDecimation" : 8, "priority" : 240, "cropLeft" : 0, @@ -148,20 +136,16 @@ ], /// The configuration for the frame-grabber, contains the following items: - /// * enable : true if the framegrabber (platform grabber) should be activated - /// * type : type of grabber. (auto|osx|dispmanx|amlogic|x11|framebuffer) [auto] + /// * type : type of grabber. (auto|osx|dispmanx|amlogic|x11|framebuffer|qt) [auto] /// * width : The width of the grabbed frames [pixels] /// * height : The height of the grabbed frames [pixels] /// * frequency_Hz : The frequency of the frame grab [Hz] - /// * priority : The priority of the frame-gabber (Default=250) HINT: lower value result in HIGHER priority! /// * ATTENTION : Power-of-Two resolution is not supported and leads to unexpected behaviour! "framegrabber" : { // for all type of grabbers - "enable" : true, "type" : "framebuffer", "frequency_Hz" : 10, - "priority" : 250, "cropLeft" : 0, "cropRight" : 0, "cropTop" : 0, @@ -171,10 +155,11 @@ "width" : 96, "height" : 96, - // valid for x11 - "useXGetImage" : false, - "horizontalPixelDecimation" : 8, - "verticalPixelDecimation" : 8, + // valid for x11|qt + "pixelDecimation" : 8, + + // valid for qt + "display" 0, // valid for framebuffer "device" : "/dev/fb0" @@ -231,14 +216,14 @@ /// The configuration of the Json/Proto forwarder. Forward messages to multiple instances of Hyperion on same and/or other hosts /// 'proto' is mostly used for video streams and 'json' for effects /// * enable : Enable or disable the forwarder (true/false) - /// * proto : Proto server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19447"] or more instances to forward ["127.0.0.1:19447","192.168.0.24:19449"] + /// * proto : Proto server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19401"] or more instances to forward ["127.0.0.1:19401","192.168.0.24:19403"] /// * json : Json server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19446"] or more instances to forward ["127.0.0.1:19446","192.168.0.24:19448"] /// HINT:If you redirect to "127.0.0.1" (localhost) you could start a second hyperion with another device/led config! - /// Be sure your client(s) is/are listening on the configured ports. The second Hyperion (if used) also needs to be configured! (HyperCon -> External -> Json Server/Proto Server) + /// Be sure your client(s) is/are listening on the configured ports. The second Hyperion (if used) also needs to be configured! (WebUI -> Settings Level (Expert) -> Configuration -> Network Services -> Forwarder) "forwarder" : { "enable" : false, - "proto" : ["127.0.0.1:19447"], + "proto" : ["127.0.0.1:19401"], "json" : ["127.0.0.1:19446"] }, @@ -249,11 +234,13 @@ "port" : 19444 }, - /// The configuration of the Proto server which enables the protobuffer remote interface - /// * port : Port at which the protobuffer server is started - "protoServer" : + /// The configuration of the Flatbuffer server which enables the Flatbuffer remote interface + /// * port : Port at which the flatbuffer server is started + "flatbufServer" : { - "port" : 19445 + "enable" : true, + "port" : 19400, + "timeout" : 5 }, /// The configuration of the boblight server which enables the boblight remote interface @@ -285,12 +272,10 @@ }, /// Configuration of the Hyperion webserver - /// * enable : enable or disable the webserver (true/false) /// * document_root : path to hyperion webapp files (webconfig developer only) /// * port : the port where hyperion webapp is accasible "webConfig" : { - "enable" : true, "document_root" : "/path/to/files", "port" : 8090 }, @@ -313,6 +298,13 @@ ] }, + "instCapture" : { + "systemEnable" : true, + "systemPriority" : 250, + "v4lEnable" : false, + "v4lPriority" : 240 + }, + /// Recreate and save led layouts made with web config. These values are just helpers for ui, not for Hyperion. "ledConfig" : { diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default index e5385f55..ae60dec6 100644 --- a/config/hyperion.config.json.default +++ b/config/hyperion.config.json.default @@ -12,9 +12,11 @@ "device" : { "type" : "file", + "hardwareLedCount" : 1, "output" : "/dev/null", "rate" : 1000000, "colorOrder" : "rgb", + "latchTime" : 1, "rewriteTime": 5000 }, @@ -26,7 +28,6 @@ { "id" : "default", "leds" : "*", - "black" : [0,0,0], "white" : [255,255,255], "red" : [255,0,0], "green" : [0,255,0], @@ -58,15 +59,9 @@ "grabberV4L2" : [ { - "enable" : false, "device" : "auto", - "input" : 0, - "standard" : "PAL", - "width" : 0, - "height" : 0, - "frameDecimation" : 2, + "standard" : "NO_CHANGE", "sizeDecimation" : 8, - "priority" : 240, "cropLeft" : 0, "cropRight" : 0, "cropTop" : 0, @@ -84,15 +79,11 @@ "framegrabber" : { - "enable" : true, "type" : "auto", "width" : 80, "height" : 45, "frequency_Hz" : 10, - "priority" : 250, - "useXGetImage" : false, - "horizontalPixelDecimation" : 8, - "verticalPixelDecimation" : 8, + "pixelDecimation" : 8, "cropLeft" : 0, "cropRight" : 0, "cropTop" : 0, @@ -132,7 +123,7 @@ { "enable" : false, "json" : ["127.0.0.1:19446"], - "proto" : ["127.0.0.1:19447"] + "proto" : ["127.0.0.1:19401"] }, "jsonServer" : @@ -140,9 +131,11 @@ "port" : 19444 }, - "protoServer" : + "flatbufServer" : { - "port" : 19445 + "enable" : true, + "port" : 19400, + "timeout" : 5 }, "boblightServer" : @@ -164,7 +157,6 @@ "webConfig" : { - "enable" : true, "document_root" : "", "port" : 8090 }, @@ -175,6 +167,13 @@ "disable": [""] }, + "instCapture" : { + "systemEnable" : true, + "systemPriority" : 250, + "v4lEnable" : false, + "v4lPriority" : 240 + }, + "ledConfig" : { "top" : 8, diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index e0443059..4efc3fcd 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -9,106 +9,44 @@ if(ENABLE_WS281XPWM) external/rpi_ws281x/rpihw.c) endif() -set(USE_SYSTEM_PROTO_LIBS ${DEFAULT_USE_SYSTEM_PROTO_LIBS} CACHE BOOL "use protobuf library from system") +set(USE_SYSTEM_FLATBUFFERS_LIBS ${DEFAULT_USE_SYSTEM_FLATBUFFERS_LIBS} CACHE BOOL "use flatbuffers library from system") -if (USE_SYSTEM_PROTO_LIBS) - find_package(Protobuf REQUIRED) - include_directories(${PROTOBUF_INCLUDE_DIRS}) +if (USE_SYSTEM_FLATBUFFERS_LIBS) + find_package(flatbuffers REQUIRED) + include_directories(${FLATBUFFERS_INCLUDE_DIRS}) else () - set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared protobuf library") - add_subdirectory(external/protobuf) + 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) if(CMAKE_CROSSCOMPILING) - # when crosscompiling import the protoc executable targets from a file generated by a native build - option(IMPORT_PROTOC "Protoc export file (protoc_export.cmake) from a native build" "IMPORT_PROTOC-FILE_NOT_FOUND") - include(${IMPORT_PROTOC}) + # when crosscompiling import the flatc executable targets from a file generated by a native build + option(IMPORT_FLATC "flatc export file (flatc_export.cmake) from a native build" "IMPORT_FLATC-FILE_NOT_FOUND") + include(${IMPORT_FLATC}) else() - # export the protoc compiler so it can be used when cross compiling - export(TARGETS protoc_compiler FILE "${CMAKE_BINARY_DIR}/protoc_export.cmake") + # export the flatc compiler so it can be used when cross compiling + export(TARGETS flatc FILE "${CMAKE_BINARY_DIR}/flatc_export.cmake") endif() - # define the include for the protobuf library at the parent scope - set(PROTOBUF_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/external/protobuf/src") - set(PROTOBUF_INCLUDE_DIRS ${PROTOBUF_INCLUDE_DIRS} PARENT_SCOPE) + # define the include for the flatbuffers library at the parent scope + set(FLATBUFFERS_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/external/flatbuffers/include") + set(FLATBUFFERS_INCLUDE_DIRS ${FLATBUFFERS_INCLUDE_DIRS} PARENT_SCOPE) - # define the protoc executable at the parent scope - get_property(PROTOBUF_PROTOC_EXECUTABLE TARGET protoc_compiler PROPERTY LOCATION) - set(PROTOBUF_PROTOC_EXECUTABLE ${PROTOBUF_PROTOC_EXECUTABLE} PARENT_SCOPE) + # define the flatc executable at the parent scope + get_property(FLATBUFFERS_FLATC_EXECUTABLE TARGET flatc PROPERTY LOCATION) + set(FLATBUFFERS_FLATC_EXECUTABLE ${FLATBUFFERS_FLATC_EXECUTABLE} PARENT_SCOPE) endif() -message(STATUS "Using protobuf compiler: " ${PROTOBUF_PROTOC_EXECUTABLE}) +message(STATUS "Using flatbuffers compiler: " ${FLATBUFFERS_FLATC_EXECUTABLE}) -#============================================================================= -# Copyright 2009 Kitware, Inc. -# Copyright 2009-2011 Philip Lowman -# Copyright 2008 Esben Mose Hansen, Ange Optimization ApS -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) -function(PROTOBUF_GENERATE_CPP SRCS HDRS) - if(NOT ARGN) - message(SEND_ERROR "Error: PROTOBUF_GENERATE_CPP() called without any proto files") - return() - endif() - if(PROTOBUF_GENERATE_CPP_APPEND_PATH) - # Create an include path for each file specified - foreach(FIL ${ARGN}) - get_filename_component(ABS_FIL ${FIL} ABSOLUTE) - get_filename_component(ABS_PATH ${ABS_FIL} PATH) - list(FIND _protobuf_include_path ${ABS_PATH} _contains_already) - if(${_contains_already} EQUAL -1) - list(APPEND _protobuf_include_path -I ${ABS_PATH}) - endif() - endforeach() - else() - set(_protobuf_include_path -I ${CMAKE_CURRENT_SOURCE_DIR}) - endif() - - if(DEFINED PROTOBUF_IMPORT_DIRS) - foreach(DIR ${PROTOBUF_IMPORT_DIRS}) - get_filename_component(ABS_PATH ${DIR} ABSOLUTE) - list(FIND _protobuf_include_path ${ABS_PATH} _contains_already) - if(${_contains_already} EQUAL -1) - list(APPEND _protobuf_include_path -I ${ABS_PATH}) - endif() - endforeach() - endif() - - if(CMAKE_CROSSCOMPILING OR USE_SYSTEM_PROTO_LIBS) - set(PROTOC_DEPENDENCY ${PROTOBUF_PROTOC_EXECUTABLE}) - else() - set(PROTOC_DEPENDENCY protoc_compiler) - endif() - - set(${SRCS}) - set(${HDRS}) - foreach(FIL ${ARGN}) - get_filename_component(ABS_FIL ${FIL} ABSOLUTE) - get_filename_component(FIL_WE ${FIL} NAME_WE) - - list(APPEND ${SRCS} "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.cc") - list(APPEND ${HDRS} "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.h") - - add_custom_command( - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.cc" - "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.pb.h" - COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} - ARGS --cpp_out ${CMAKE_CURRENT_BINARY_DIR} ${_protobuf_include_path} ${ABS_FIL} - DEPENDS ${ABS_FIL} ${PROTOC_DEPENDENCY} - COMMENT "Running C++ protocol buffer compiler on ${FIL}" - VERBATIM - ) - endforeach() - - set_source_files_properties(${${SRCS}} ${${HDRS}} PROPERTIES GENERATED TRUE) - set(${SRCS} ${${SRCS}} PARENT_SCOPE) - set(${HDRS} ${${HDRS}} PARENT_SCOPE) +function(compile_flattbuffer_schema SRC_FBS OUTPUT_DIR) + string(REGEX REPLACE "\\.fbs$" "_generated.h" GEN_HEADER ${SRC_FBS}) + add_custom_command( + OUTPUT ${GEN_HEADER} + COMMAND "${FLATBUFFERS_FLATC_EXECUTABLE}" -c --no-includes --gen-mutable + --gen-object-api + -o "${OUTPUT_DIR}" + "${SRC_FBS}" + DEPENDS flatc) endfunction() diff --git a/dependencies/build/tinkerforge/ip_connection.c b/dependencies/build/tinkerforge/ip_connection.c index 31cf4aee..62278442 100644 --- a/dependencies/build/tinkerforge/ip_connection.c +++ b/dependencies/build/tinkerforge/ip_connection.c @@ -7,7 +7,7 @@ */ #ifndef _WIN32 - #define _BSD_SOURCE // for usleep from unistd.h + #define _DEFAULT_SOURCE // for usleep from unistd.h #endif #include diff --git a/dependencies/external/flatbuffers b/dependencies/external/flatbuffers new file mode 160000 index 00000000..0eb7b3be --- /dev/null +++ b/dependencies/external/flatbuffers @@ -0,0 +1 @@ +Subproject commit 0eb7b3beb037748bf5b469e4df9db862c4833e35 diff --git a/dependencies/external/protobuf b/dependencies/external/protobuf deleted file mode 160000 index adce8a99..00000000 --- a/dependencies/external/protobuf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit adce8a99fdab90f290d659b6b3bf2d09b721e24a diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index dc5e88fd..93601451 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -1,32 +1,34 @@ +option(BUILD_HYPERION_DOC "Build hyperion documentation" OFF) -# Find doxygen +# Find doxygen and check if Doxygen is installed find_package(Doxygen QUIET) -# This processes our hyperion-cmake.doxyfile and subsitutes variables to generate a final hyperion.doxyfile -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/hyperion.in.doxygen ${CMAKE_CURRENT_BINARY_DIR}/hyperion.doxygen) +if (BUILD_HYPERION_DOC) + if (DOXYGEN_FOUND) -# This processes the shell script that is used to build the documentation and check the result -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/hyperion-build-doc.in.sh ${CMAKE_CURRENT_BINARY_DIR}/hyperion-build-doc.sh) + # set input and output files + set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/hyperion.in.doxygen) + set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/hyperion.doxygen) -# Define all static (i.e. not generated) documentation files -set(StaticDocumentationFiles hyperion-header.html hyperion-footer.html hyperion-stylesheet.css) + # request to configure the file + configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY) + message(STATUS "Doxygen build started") -# Loop over all static documentation files -foreach(StaticDocumentationFile ${StaticDocumentationFiles}) - # Copy the file to the bindary documentation directory - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${StaticDocumentationFile} ${CMAKE_CURRENT_BINARY_DIR}/html/${StaticDocumentationFile} COPYONLY) -endforeach() + # Define all static (i.e. not generated) documentation files + set(StaticDocumentationFiles hyperion-footer.html) -if(DOXYGEN_FOUND) - option(BuildDocumentationSearchEngine "Enable doxygen's search engine (requires that documentation to be installed on a php enabled web server)" OFF) - if(BuildDocumentationSearchEngine) - set(DOXYGEN_SEARCHENGINE YES) - else(BuildDocumentationSearchEngine) - set(DOXYGEN_SEARCHENGINE NO) - endif(BuildDocumentationSearchEngine) + # Loop over all static documentation files + foreach(StaticDocumentationFile ${StaticDocumentationFiles}) + # Copy the file to the bindary documentation directory + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${StaticDocumentationFile} ${CMAKE_CURRENT_BINARY_DIR}/html/${StaticDocumentationFile} COPYONLY) + endforeach() - #Create a custom target to build documentation. It runs doxygen aginast the generated hyperion.doxyfile and checks its return value - add_custom_target(doc sh ${CMAKE_CURRENT_BINARY_DIR}/hyperion-build-doc.sh) -else(DOXYGEN_FOUND) - message(WARNING "Doxygen not found, unable to generate documenation!") -endif(DOXYGEN_FOUND) + add_custom_target( doc_doxygen ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM ) + else(DOXYGEN_FOUND) + message(WARNING "Doxygen not found, unable to generate documenation!") + endif(DOXYGEN_FOUND) +endif() diff --git a/doc/hyperion-build-doc.in.sh b/doc/hyperion-build-doc.in.sh deleted file mode 100644 index 3198611d..00000000 --- a/doc/hyperion-build-doc.in.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -# Fail on error. -set -e - -# Log file containing documentation errors and warnings (if any). -log_file=${CMAKE_CURRENT_BINARY_DIR}/hyperion-doxygen.log - -# Remove the log file before building the documentation. -rm -f $log_file - -# Generate the documentation. -${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/hyperion.doxygen - -# At this point, the log file should have been generated. -# If not, an error is displayed on stderr and 1 is returned to indicate an error. -if [ -f $log_file ] ; then - # So the log file exists. If its size is > 0, show its contents on stderr and exit 1. - if [ -s $log_file ] ; then - cat $log_file 1>&2 - exit 1; - else - # The log file exists, but its size is zero, meaning there were no documentation warnings or errors. - # Exit with 0 to indicate success. - exit 0; - fi -else - echo "The doxygen log file ($log_file) does not exist. Ensure that WARN_LOGFILE is set correctly in hyperion-cmake.doxyfile." 1>&2 - exit 1; -fi diff --git a/doc/hyperion-header.html b/doc/hyperion-header.html deleted file mode 100644 index 652afa69..00000000 --- a/doc/hyperion-header.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - $title - - - - diff --git a/doc/hyperion-stylesheet.css b/doc/hyperion-stylesheet.css deleted file mode 100644 index 2e81b75d..00000000 --- a/doc/hyperion-stylesheet.css +++ /dev/null @@ -1,472 +0,0 @@ -BODY,H1,H2,H3,H4,H5,H6,P,CENTER,TD,TH,UL,DL,DIV { - font-family: Geneva, Arial, Helvetica, sans-serif; -} -BODY,TD { - font-size: 90%; -} -H1 { - text-align: center; - font-size: 160%; -} -H2 { - font-size: 120%; -} -H3 { - font-size: 100%; -} -CAPTION { - font-weight: bold -} -DIV.qindex { - width: 100%; - background-color: #e8eef2; - border: 1px solid #84b0c7; - text-align: center; - margin: 2px; - padding: 2px; - line-height: 140%; -} -DIV.navpath { - width: 100%; - background-color: #e8eef2; - border: 1px solid #84b0c7; - text-align: center; - margin: 2px; - padding: 2px; - line-height: 140%; -} -DIV.navtab { - background-color: #e8eef2; - border: 1px solid #84b0c7; - text-align: center; - margin: 2px; - margin-right: 15px; - padding: 2px; -} -TD.navtab { - font-size: 70%; -} -A.qindex { - text-decoration: none; - font-weight: bold; - color: #1A419D; -} -A.qindex:visited { - text-decoration: none; - font-weight: bold; - color: #1A419D -} -A.qindex:hover { - text-decoration: none; - background-color: #ddddff; -} -A.qindexHL { - text-decoration: none; - font-weight: bold; - background-color: #6666cc; - color: #ffffff; - border: 1px double #9295C2; -} -A.qindexHL:hover { - text-decoration: none; - background-color: #6666cc; - color: #ffffff; -} -A.qindexHL:visited { - text-decoration: none; - background-color: #6666cc; - color: #ffffff -} -A.el { - text-decoration: none; - font-weight: bold -} -A.elRef { - font-weight: bold -} -A.code:link { - text-decoration: none; - font-weight: normal; - color: #0000FF -} -A.code:visited { - text-decoration: none; - font-weight: normal; - color: #0000FF -} -A.codeRef:link { - font-weight: normal; - color: #0000FF -} -A.codeRef:visited { - font-weight: normal; - color: #0000FF -} -A:hover { - text-decoration: none; - background-color: #f2f2ff -} -DL.el { - margin-left: -1cm -} -.fragment { - font-family: monospace, fixed; - font-size: 95%; -} -PRE.fragment { - border: 1px solid #CCCCCC; - background-color: #f5f5f5; - margin-top: 4px; - margin-bottom: 4px; - margin-left: 2px; - margin-right: 8px; - padding-left: 6px; - padding-right: 6px; - padding-top: 4px; - padding-bottom: 4px; -} -DIV.ah { - background-color: black; - font-weight: bold; - color: #ffffff; - margin-bottom: 3px; - margin-top: 3px -} - -DIV.groupHeader { - margin-left: 16px; - margin-top: 12px; - margin-bottom: 6px; - font-weight: bold; -} -DIV.groupText { - margin-left: 16px; - font-style: italic; - font-size: 90% -} -BODY { - background: white; - color: black; - margin-right: 20px; - margin-left: 20px; -} -TD.indexkey { - background-color: #e8eef2; - font-weight: bold; - padding-right : 10px; - padding-top : 2px; - padding-left : 10px; - padding-bottom : 2px; - margin-left : 0px; - margin-right : 0px; - margin-top : 2px; - margin-bottom : 2px; - border: 1px solid #CCCCCC; -} -TD.indexvalue { - background-color: #e8eef2; - font-style: italic; - padding-right : 10px; - padding-top : 2px; - padding-left : 10px; - padding-bottom : 2px; - margin-left : 0px; - margin-right : 0px; - margin-top : 2px; - margin-bottom : 2px; - border: 1px solid #CCCCCC; -} -TR.memlist { - background-color: #f0f0f0; -} -P.formulaDsp { - text-align: center; -} -IMG.formulaDsp { -} -IMG.formulaInl { - vertical-align: middle; -} -SPAN.keyword { color: #008000 } -SPAN.keywordtype { color: #604020 } -SPAN.keywordflow { color: #e08000 } -SPAN.comment { color: #800000 } -SPAN.preprocessor { color: #806020 } -SPAN.stringliteral { color: #002080 } -SPAN.charliteral { color: #008080 } -SPAN.vhdldigit { color: #ff00ff } -SPAN.vhdlchar { color: #000000 } -SPAN.vhdlkeyword { color: #700070 } -SPAN.vhdllogic { color: #ff0000 } - -.mdescLeft { - padding: 0px 8px 4px 8px; - font-size: 80%; - font-style: italic; - background-color: #FAFAFA; - border-top: 1px none #E0E0E0; - border-right: 1px none #E0E0E0; - border-bottom: 1px none #E0E0E0; - border-left: 1px none #E0E0E0; - margin: 0px; -} -.mdescRight { - padding: 0px 8px 4px 8px; - font-size: 80%; - font-style: italic; - background-color: #FAFAFA; - border-top: 1px none #E0E0E0; - border-right: 1px none #E0E0E0; - border-bottom: 1px none #E0E0E0; - border-left: 1px none #E0E0E0; - margin: 0px; -} -.memItemLeft { - padding: 1px 0px 0px 8px; - margin: 4px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-top-color: #E0E0E0; - border-right-color: #E0E0E0; - border-bottom-color: #E0E0E0; - border-left-color: #E0E0E0; - border-top-style: solid; - border-right-style: none; - border-bottom-style: none; - border-left-style: none; - background-color: #FAFAFA; - font-size: 80%; -} -.memItemRight { - padding: 1px 8px 0px 8px; - margin: 4px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-top-color: #E0E0E0; - border-right-color: #E0E0E0; - border-bottom-color: #E0E0E0; - border-left-color: #E0E0E0; - border-top-style: solid; - border-right-style: none; - border-bottom-style: none; - border-left-style: none; - background-color: #FAFAFA; - font-size: 80%; -} -.memTemplItemLeft { - padding: 1px 0px 0px 8px; - margin: 4px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-top-color: #E0E0E0; - border-right-color: #E0E0E0; - border-bottom-color: #E0E0E0; - border-left-color: #E0E0E0; - border-top-style: none; - border-right-style: none; - border-bottom-style: none; - border-left-style: none; - background-color: #FAFAFA; - font-size: 80%; -} -.memTemplItemRight { - padding: 1px 8px 0px 8px; - margin: 4px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-top-color: #E0E0E0; - border-right-color: #E0E0E0; - border-bottom-color: #E0E0E0; - border-left-color: #E0E0E0; - border-top-style: none; - border-right-style: none; - border-bottom-style: none; - border-left-style: none; - background-color: #FAFAFA; - font-size: 80%; -} -.memTemplParams { - padding: 1px 0px 0px 8px; - margin: 4px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - border-top-color: #E0E0E0; - border-right-color: #E0E0E0; - border-bottom-color: #E0E0E0; - border-left-color: #E0E0E0; - border-top-style: solid; - border-right-style: none; - border-bottom-style: none; - border-left-style: none; - color: #606060; - background-color: #FAFAFA; - font-size: 80%; -} -.search { - color: #003399; - font-weight: bold; -} -FORM.search { - margin-bottom: 0px; - margin-top: 0px; -} -INPUT.search { - font-size: 75%; - color: #000080; - font-weight: normal; - background-color: #e8eef2; -} -TD.tiny { - font-size: 75%; -} -a { - color: #1A41A8; -} -a:visited { - color: #2A3798; -} -.dirtab { - padding: 4px; - border-collapse: collapse; - border: 1px solid #84b0c7; -} -TH.dirtab { - background: #e8eef2; - font-weight: bold; -} -HR { - height: 1px; - border: none; - border-top: 1px solid black; -} - -/* Style for detailed member documentation */ -.memtemplate { - font-size: 80%; - color: #606060; - font-weight: normal; - margin-left: 3px; -} -.memnav { - background-color: #e8eef2; - border: 1px solid #84b0c7; - text-align: center; - margin: 2px; - margin-right: 15px; - padding: 2px; -} -.memitem { - padding: 4px; - background-color: #eef3f5; - border-width: 1px; - border-style: solid; - border-color: #dedeee; - -moz-border-radius: 8px 8px 8px 8px; -} -.memname { - white-space: nowrap; - font-weight: bold; -} -.memdoc { - padding-left: 10px; -} -.memproto { - background-color: #d5e1e8; - width: 100%; - border-width: 1px; - border-style: solid; - border-color: #84b0c7; - font-weight: bold; - -moz-border-radius: 8px 8px 8px 8px; -} -.paramkey { - text-align: right; -} -.paramtype { - white-space: nowrap; -} -.paramname { - color: #602020; - font-style: italic; - white-space: nowrap; -} -/* End Styling for detailed member documentation */ - -/* for the tree view */ -.ftvtree { - font-family: sans-serif; - margin:0.5em; -} -/* these are for tree view when used as main index */ -.directory { - font-size: 9pt; - font-weight: bold; -} -.directory h3 { - margin: 0px; - margin-top: 1em; - font-size: 11pt; -} - -/* The following two styles can be used to replace the root node title */ -/* with an image of your choice. Simply uncomment the next two styles, */ -/* specify the name of your image and be sure to set 'height' to the */ -/* proper pixel height of your image. */ - -/* .directory h3.swap { */ -/* height: 61px; */ -/* background-repeat: no-repeat; */ -/* background-image: url("yourimage.gif"); */ -/* } */ -/* .directory h3.swap span { */ -/* display: none; */ -/* } */ - -.directory > h3 { - margin-top: 0; -} -.directory p { - margin: 0px; - white-space: nowrap; -} -.directory div { - display: none; - margin: 0px; -} -.directory img { - vertical-align: -30%; -} -/* these are for tree view when not used as main index */ -.directory-alt { - font-size: 100%; - font-weight: bold; -} -.directory-alt h3 { - margin: 0px; - margin-top: 1em; - font-size: 11pt; -} -.directory-alt > h3 { - margin-top: 0; -} -.directory-alt p { - margin: 0px; - white-space: nowrap; -} -.directory-alt div { - display: none; - margin: 0px; -} -.directory-alt img { - vertical-align: -30%; -} diff --git a/doc/hyperion.in.doxygen b/doc/hyperion.in.doxygen index 5c91fc5f..69c37750 100644 --- a/doc/hyperion.in.doxygen +++ b/doc/hyperion.in.doxygen @@ -58,7 +58,7 @@ PROJECT_LOGO = # entered, it will be relative to the location where doxygen was started. If # left blank the current directory will be used. -OUTPUT_DIRECTORY = "${CMAKE_CURRENT_BINARY_DIR}" +OUTPUT_DIRECTORY = @CMAKE_CURRENT_BINARY_DIR@ # If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 4096 sub- # directories (in 2 levels) under the output directory of each output format and @@ -152,7 +152,7 @@ FULL_PATH_NAMES = YES # will be relative from the directory where doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. -STRIP_FROM_PATH = ${CMAKE_SOURCE_DIR} +STRIP_FROM_PATH = @CMAKE_SOURCE_DIR@ # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the # path mentioned in the documentation of a class, which tells the reader which @@ -409,7 +409,7 @@ LOOKUP_CACHE_SIZE = 0 # normally produced when WARNINGS is set to YES. # The default value is: NO. -EXTRACT_ALL = NO +EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES all private members of a class will # be included in the documentation. @@ -742,7 +742,7 @@ WARN_FORMAT = "$file:$line: $text" # messages should be written. If left blank the output is written to standard # error (stderr). -WARN_LOGFILE = ${CMAKE_CURRENT_BINARY_DIR}/hyperion-doxygen.log +WARN_LOGFILE = @CMAKE_CURRENT_BINARY_DIR@/hyperion-doxygen.log #--------------------------------------------------------------------------- # Configuration options related to the input files @@ -754,10 +754,10 @@ WARN_LOGFILE = ${CMAKE_CURRENT_BINARY_DIR}/hyperion-doxygen.log # spaces. # Note: If this tag is empty the current directory is searched. -INPUT = "${CMAKE_SOURCE_DIR}/include" \ - "${CMAKE_SOURCE_DIR}/libsrc" \ - "${CMAKE_SOURCE_DIR}/src" \ - "${CMAKE_SOURCE_DIR}/test" +INPUT = @CMAKE_SOURCE_DIR@/include \ + @CMAKE_SOURCE_DIR@/libsrc \ + @CMAKE_SOURCE_DIR@/src \ + @CMAKE_SOURCE_DIR@/test # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -1050,7 +1050,7 @@ HTML_FILE_EXTENSION = .html # of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_HEADER = "html/hyperion-header.html" +HTML_HEADER = # The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each # generated HTML page. If the tag is left blank doxygen will generate a standard @@ -1482,7 +1482,7 @@ MATHJAX_CODEFILE = # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. -SEARCHENGINE = ${DOXYGEN_SEARCHENGINE} +SEARCHENGINE = NO # When the SERVER_BASED_SEARCH tag is enabled the search engine will be # implemented using a web server instead of a web client using Javascript. There @@ -2080,7 +2080,7 @@ DOT_NUM_THREADS = 0 # The default value is: Helvetica. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTNAME = FreeSans +DOT_FONTNAME = # The DOT_FONTSIZE tag can be used to set the size (in points) of the font of # dot graphs. @@ -2234,7 +2234,7 @@ INTERACTIVE_SVG = NO # found. If left blank, it is assumed the dot tool can be found in the path. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_PATH = "${DOXYGEN_DOT_PATH}" +DOT_PATH = "" # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the \dotfile diff --git a/effects/Seawaves.json b/effects/Seawaves.json new file mode 100644 index 00000000..cf3c30c9 --- /dev/null +++ b/effects/Seawaves.json @@ -0,0 +1,47 @@ +{ + "args": { + "center_x": 1.25, + "center_y": -0.25, + "colors": [ + [ + 8, + 0, + 255 + ], + [ + 0, + 161, + 255 + ], + [ + 0, + 222, + 255 + ], + [ + 0, + 153, + 255 + ], + [ + 38, + 0, + 255 + ], + [ + 0, + 199, + 255 + ] + ], + "random-center": false, + "reverse": false, + "reverse_time": 0, + "rotation_time": 60, + "smoothing-custom-settings": true, + "smoothing-time_ms": 200, + "smoothing-updateFrequency": 25 + }, + "name": "Sea waves", + "script": "waves.py" +} diff --git a/effects/Waves.json b/effects/Waves.json new file mode 100644 index 00000000..b4ae8b14 --- /dev/null +++ b/effects/Waves.json @@ -0,0 +1,8 @@ +{ + "name" : "Waves with Color", + "script" : "waves.py", + "args" : + { + "reverse" : false + } +} diff --git a/effects/gif.py b/effects/gif.py index 4463f782..a210f4ff 100644 --- a/effects/gif.py +++ b/effects/gif.py @@ -6,8 +6,10 @@ framesPerSecond = float(hyperion.args.get('fps', 25)) reverse = bool(hyperion.args.get('reverse', False)) sleepTime = 1./framesPerSecond +imageList = [] + if imageFile: - imageList = list(reversed(hyperion.getImage(imageFile))) if reverse else hyperion.getImage(imageFile) + imageList = [reversed(hyperion.getImage(imageFile))] if reverse else hyperion.getImage(imageFile) # Start the write data loop while not hyperion.abort() and imageList: diff --git a/effects/running_dots.json b/effects/running_dots.json deleted file mode 100644 index 8accd451..00000000 --- a/effects/running_dots.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name" : "Running dots", - "script" : "running_dots.py", - "args" : - { - "speed" : 1.5, - "whiteLevel" : 100, - "colorLevel" : 230 - } -} diff --git a/effects/schema/gif.schema.json b/effects/schema/gif.schema.json index 0a61dc11..3161e3ac 100644 --- a/effects/schema/gif.schema.json +++ b/effects/schema/gif.schema.json @@ -3,11 +3,16 @@ "script" : "gif.py", "title":"edt_eff_gif_header", "required":true, - "properties":{ + "properties": { "image": { "type": "string", "title":"edt_eff_image", - "format" : "file", + "format" : "url", + "options" : + { + "upload" : true, + "auto_upload" : true + }, "default": "", "propertyOrder" : 1 }, diff --git a/effects/schema/running_dots.schema.json b/effects/schema/running_dots.schema.json deleted file mode 100644 index 5661bde7..00000000 --- a/effects/schema/running_dots.schema.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "type":"object", - "script" : "running_dots.py", - "title":"edt_eff_runningdots_header", - "required":true, - "properties":{ - "speed": { - "type": "number", - "title":"edt_eff_speed", - "default": 1.5, - "minimum" : 0.1, - "propertyOrder" : 1 - }, - "colorLevel": { - "type": "integer", - "title":"edt_eff_colorevel", - "default": 220, - "minimium" : 0, - "maximum" : 255, - "propertyOrder" : 2 - }, - "whiteLevel": { - "type": "integer", - "title":"edt_eff_whitelevel", - "default": 0, - "minimium" : 0, - "maximum" : 254, - "propertyOrder" : 3 - } - }, - "additionalProperties": false -} diff --git a/effects/schema/waves.schema.json b/effects/schema/waves.schema.json new file mode 100644 index 00000000..10da7c8a --- /dev/null +++ b/effects/schema/waves.schema.json @@ -0,0 +1,121 @@ +{ + "type":"object", + "script" : "waves.py", + "title":"edt_eff_waves_header", + "required":true, + "properties":{ + "reverse": { + "type": "boolean", + "title":"edt_eff_reversedirection", + "default": false, + "propertyOrder" : 1 + }, + "reverse_time": { + "type": "integer", + "title":"edt_eff_reverseRandomTime", + "default": 0, + "minimum": 0, + "append" : "edt_append_s", + "propertyOrder" : 2 + }, + "rotation_time": { + "type": "integer", + "title":"edt_eff_rotationtime", + "default": 50, + "minimum": 1, + "append" : "edt_append_s", + "propertyOrder" : 2 + }, + "random-center": { + "type": "boolean", + "title":"edt_eff_randomCenter", + "default": false, + "propertyOrder" : 3 + }, + "center_x": { + "type": "number", + "title":"edt_eff_centerx", + "default": -0.15, + "minimum" : -3.0, + "maximum" : 4.0, + "step" : 0.1, + "options": { + "dependencies": { + "random-center": false + } + }, + "propertyOrder" : 4 + }, + "center_y": { + "type": "number", + "title":"edt_eff_centery", + "default": -0.15, + "minimum" : -3.0, + "maximum" : 4.0, + "step" : 0.1, + "options": { + "dependencies": { + "random-center": false + } + }, + "propertyOrder" : 5 + }, + "smoothing-custom-settings" : + { + "type" : "boolean", + "title" : "edt_eff_smooth_custom", + "default" : true, + "propertyOrder" : 6 + }, + "smoothing-time_ms" : + { + "type" : "integer", + "title" : "edt_eff_smooth_time_ms", + "minimum" : 25, + "maximum": 600, + "default" : 200, + "append" : "edt_append_ms", + "options": { + "dependencies": { + "smoothing-custom-settings": true + } + }, + "propertyOrder" : 7 + }, + "smoothing-updateFrequency" : + { + "type" : "number", + "title" : "edt_eff_smooth_updateFrequency", + "minimum" : 1.0, + "maximum" : 100.0, + "default" : 25.0, + "append" : "edt_append_hz", + "options": { + "dependencies": { + "smoothing-custom-settings": true + } + }, + "propertyOrder" : 8 + }, + "colors": { + "type": "array", + "title":"edt_eff_customColor", + "items" : { + "type": "array", + "title" : "edt_eff_color", + "format":"colorpicker", + "default" : [255,0,0], + "items":{ + "type":"integer", + "minimum": 0, + "maximum": 255 + }, + "minItems": 3, + "maxItems": 3 + }, + "minItems": 6, + "propertyOrder" : 9 + } + }, + "additionalProperties": false +} diff --git a/effects/waves.py b/effects/waves.py new file mode 100644 index 00000000..b253ec79 --- /dev/null +++ b/effects/waves.py @@ -0,0 +1,77 @@ +import hyperion, time, math, random + +randomCenter = bool(hyperion.args.get('random-center', False)) +centerX = float(hyperion.args.get('center_x', -0.15)) +centerY = float(hyperion.args.get('center_y', -0.25)) +rotationTime = float(hyperion.args.get('rotation_time', 90)) +colors = hyperion.args.get('colors', ((255,0,0),(255,255,0),(0,255,0),(0,255,255),(0,0,255),(255,0,255))) +reverse = bool(hyperion.args.get('reverse', False)) +reverseTime = int(hyperion.args.get('reverse_time', 0)) +#rotate = bool(hyperion.args.get('rotate', True)) +positions = [] + +# calc center if random +if randomCenter: + centerX = random.uniform(0.0, 1.0) + centerY = random.uniform(0.0, 1.0) + +rCenterX = int(round(float(hyperion.imageWidth())*centerX)) +rCenterY = int(round(float(hyperion.imageHeight())*centerY)) + +#calc interval +sleepTime = max(1/(255/rotationTime), 0.016) + +#calc diagonal +if centerX < 0.5: + cX = 1.0-centerX +else: + cX = 0.0+centerX + +if centerY < 0.5: + cY = 1.0-centerY +else: + cY = 0.0+centerY + +diag = int(round(math.sqrt(((cX*hyperion.imageWidth())**2)+((cY*hyperion.imageHeight())**2)))) +# some diagonal overhead +diag = int(diag*1.3) + +# calc positions +pos = 0 +step = int(255/len(colors)) +for entry in colors: + positions.append(pos) + pos += step + +# target time +targetTime = time.time()+float(reverseTime) + +#hyperion.imageCOffset(int(hyperion.imageWidth()/2), int(hyperion.imageHeight()/2)) + +while not hyperion.abort(): + # verify reverseTime, randomize reverseTime based on reverseTime up to reversedTime*2 + if reverseTime >= 1: + now = time.time() + if now > targetTime: + reverse = not reverse + targetTime = time.time()+random.uniform(float(reverseTime), float(reverseTime*2.0)) + # apply rotate + #if rotate: + # hyperion.imageCRotate(1) + # prepare bytearray with colors and positions + gradientBa = bytearray() + it = 0 + for color in colors: + gradientBa += bytearray((positions[it],color[0],color[1],color[2])) + it += 1 + + hyperion.imageRadialGradient(rCenterX,rCenterY, diag, gradientBa,0) + + # increment positions + for i, pos in enumerate(positions): + if reverse: + positions[i] = pos - 1 if pos >= 1 else 255 + else: + positions[i] = pos + 1 if pos <= 254 else 0 + hyperion.imageShow() + time.sleep(sleepTime) diff --git a/include/utils/JsonProcessor.h b/include/api/JsonAPI.h similarity index 77% rename from include/utils/JsonProcessor.h rename to include/api/JsonAPI.h index 5ccd8e81..c1aa19e4 100644 --- a/include/utils/JsonProcessor.h +++ b/include/api/JsonAPI.h @@ -7,36 +7,13 @@ #include // qt includess -#include #include #include #include -// createEffect helper -struct find_schema: std::unary_function -{ - QString pyFile; - find_schema(QString pyFile):pyFile(pyFile) { } - bool operator()(EffectSchema const& schema) const - { - return schema.pyFile == pyFile; - } -}; +class JsonCB; -// deleteEffect helper -struct find_effect: std::unary_function -{ - QString effectName; - find_effect(QString effectName) :effectName(effectName) { } - bool operator()(EffectDefinition const& effectDefinition) const - { - return effectDefinition.name == effectName; - } -}; - -class ImageProcessor; - -class JsonProcessor : public QObject +class JsonAPI : public QObject { Q_OBJECT @@ -49,7 +26,7 @@ public: /// @param parent Parent QObject /// @param noListener if true, this instance won't listen for hyperion push events /// - JsonProcessor(QString peerAddress, Logger* log, QObject* parent, bool noListener = false); + JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListener = false); /// /// Handle an incoming JSON message @@ -58,26 +35,20 @@ public: /// void handleMessage(const QString & message); - /// - /// send a forced serverinfo to a client - /// - void forceServerInfo(); - public slots: - /// _timer_ledcolors requests ledcolor updates (if enabled) - void streamLedcolorsUpdate(); + /// + /// @brief is called whenever the current Hyperion instance pushes new led raw values (if enabled) + /// @param ledColors The current ledColors + /// + void streamLedcolorsUpdate(const std::vector& ledColors); /// push images whenever hyperion emits (if enabled) - void setImage(int priority, const Image & image, int duration_ms); + void setImage(const Image & image); /// process and push new log messages from logger (if enabled) void incommingLogMessage(Logger::T_LOG_MESSAGE); signals: - /// - /// Signal which is emitted when a sendSuccessReply() has been executed - /// - void pushReq(); /// /// Signal emits with the reply message provided with handleMessage() /// @@ -89,6 +60,9 @@ signals: void forwardJsonMessage(QJsonObject); private: + // true if further callbacks are forbidden (http) + bool _noListener; + /// The peer address of the client QString _peerAddress; @@ -98,17 +72,8 @@ private: /// Hyperion instance Hyperion* _hyperion; - /// The processor for translating images to led-values - ImageProcessor * _imageProcessor; - - /// holds the state before off state - static std::map _componentsPrevState; - - /// returns if hyperion is on or off - inline bool hyperionIsActive() { return JsonProcessor::_componentsPrevState.empty(); }; - - /// timer for ledcolors streaming - QTimer _timer_ledcolors; + // The JsonCB instance which handles data subscription/notifications + JsonCB* _jsonCB; // streaming buffers QJsonObject _streaming_leds_reply; @@ -121,9 +86,15 @@ private: /// mutex to determine state of image streaming QMutex _image_stream_mutex; + /// mutex to determine state of image streaming + QMutex _led_stream_mutex; + /// timeout for live video refresh volatile qint64 _image_stream_timeout; + /// timeout for led color refresh + volatile qint64 _led_stream_timeout; + /// /// Handle an incoming JSON Color message /// @@ -143,7 +114,7 @@ private: /// /// @param message the incoming message /// - void handleEffectCommand(const QJsonObject & message, const QString &command, const int tan); + void handleEffectCommand(const QJsonObject &message, const QString &command, const int tan); /// /// Handle an incoming JSON Effect message (Write JSON Effect) @@ -180,13 +151,6 @@ private: /// void handleClearCommand(const QJsonObject & message, const QString &command, const int tan); - /// - /// Handle an incoming JSON Clearall message - /// - /// @param message the incoming message - /// - void handleClearallCommand(const QJsonObject & message, const QString &command, const int tan); - /// /// Handle an incoming JSON Adjustment message /// @@ -213,12 +177,6 @@ private: /// void handleSchemaGetCommand(const QJsonObject & message, const QString &command, const int tan); - /// Handle an incoming JSON GetConfig message from handleConfigCommand() - /// - /// @param message the incoming message - /// - void handleConfigGetCommand(const QJsonObject & message, const QString &command, const int tan); - /// Handle an incoming JSON SetConfig message from handleConfigCommand() /// /// @param message the incoming message @@ -256,6 +214,13 @@ private: /// void handleVideoModeCommand(const QJsonObject & message, const QString &command, const int tan); + /// + /// Handle an incoming JSON Clearall message + /// + /// @param message the incoming message + /// + void handleClearallCommand(const QJsonObject & message, const QString &command, const int tan); + /// /// Handle an incoming JSON message of unknown type /// @@ -266,6 +231,11 @@ private: /// void sendSuccessReply(const QString &command="", const int tan=0); + /// + /// Send a standard reply indicating success with data + /// + void sendSuccessDataReply(const QJsonDocument &doc, const QString &command="", const int &tan=0); + /// /// Send an error message back to the client /// diff --git a/include/api/JsonCB.h b/include/api/JsonCB.h new file mode 100644 index 00000000..98d1c3bb --- /dev/null +++ b/include/api/JsonCB.h @@ -0,0 +1,112 @@ +#pragma once + +// qt incl +#include +#include + +// components def +#include +// bonjour +#include +// videModes +#include +// settings +#include + +class Hyperion; +class ComponentRegister; +class BonjourBrowserWrapper; +class PriorityMuxer; + +class JsonCB : public QObject +{ + Q_OBJECT + +public: + JsonCB(QObject* parent); + + /// + /// @brief Subscribe to future data updates given by cmd + /// @param cmd The cmd which will be subscribed for + /// @return True on success, false if not found + /// + bool subscribeFor(const QString& cmd); + + /// + /// @brief Get all possible commands to subscribe for + /// @return The list of commands + /// + QStringList getCommands() { return _availableCommands; }; + /// + /// @brief Get all subscribed commands + /// @return The list of commands + /// + QStringList getSubscribedCommands() { return _subscribedCommands; }; +signals: + /// + /// @brief Emits whenever a new json mesage callback is ready to send + /// @param The JsonObject message + /// + void newCallback(QJsonObject); + +private slots: + /// + /// @brief handle component state changes + /// + void handleComponentState(const hyperion::Components comp, const bool state); + + /// + /// @brief handle emits from bonjour wrapper + /// @param bRegisters The full register map + /// + void handleBonjourChange(const QMap& bRegisters); + + /// + /// @brief handle emits from PriorityMuxer + /// + void handlePriorityUpdate(); + + /// + /// @brief Handle imageToLedsMapping updates + /// + void handleImageToLedsMappingChange(const int& mappingType); + + /// + /// @brief Handle the adjustment update + /// + void handleAdjustmentChange(); + + /// + /// @brief Handle video mode change + /// @param mode The new videoMode + /// + void handleVideoModeChange(const VideoMode& mode); + + /// + /// @brief Handle effect list change + /// + void handleEffectListChange(); + + /// + /// @brief Handle a config part change. This does NOT include (global) changes from other hyperion instances + /// @param type The settings type from enum + /// @param data The data as QJsonDocument + /// + void handleSettingsChange(const settings::type& type, const QJsonDocument& data); + +private: + /// pointer of Hyperion instance + Hyperion* _hyperion; + /// pointer of comp register + ComponentRegister* _componentRegister; + /// Bonjour instance + BonjourBrowserWrapper* _bonjour; + /// 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); +}; diff --git a/include/blackborder/BlackBorderDetector.h b/include/blackborder/BlackBorderDetector.h index 7323ebd1..d92edb3c 100644 --- a/include/blackborder/BlackBorderDetector.h +++ b/include/blackborder/BlackBorderDetector.h @@ -89,10 +89,9 @@ namespace hyperion // find first X pixel of the image for (int x = 0; x < width33percent; ++x) { - const Pixel_T & color1 = image( (width - x), yCenter); // right side center line check - const Pixel_T & color2 = image(x, height33percent); - const Pixel_T & color3 = image(x, height66percent); - if (!isBlack(color1) || !isBlack(color2) || !isBlack(color3)) + if (!isBlack(image((width - x), yCenter)) + || !isBlack(image(x, height33percent)) + || !isBlack(image(x, height66percent))) { firstNonBlackXPixelIndex = x; break; @@ -102,10 +101,9 @@ namespace hyperion // find first Y pixel of the image for (int y = 0; y < height33percent; ++y) { - const Pixel_T & color1 = image(xCenter, (height - y)); // bottom center line check - const Pixel_T & color2 = image(width33percent, y ); - const Pixel_T & color3 = image(width66percent, y); - if (!isBlack(color1) || !isBlack(color2) || !isBlack(color3)) + if (!isBlack(image(xCenter, (height - y))) + || !isBlack(image(width33percent, y)) + || !isBlack(image(width66percent, y))) { firstNonBlackYPixelIndex = y; break; @@ -203,10 +201,9 @@ namespace hyperion int x; for (x = 0; x < width33percent; ++x) { - const Pixel_T & color1 = image( (width - x), yCenter); // right side center line check - const Pixel_T & color2 = image(x, height33percent); - const Pixel_T & color3 = image(x, height66percent); - if (!isBlack(color1) || !isBlack(color2) || !isBlack(color3)) + if (!isBlack(image((width - x), yCenter)) + || !isBlack(image(x, height33percent)) + || !isBlack(image(x, height66percent))) { firstNonBlackXPixelIndex = x; break; @@ -216,13 +213,13 @@ namespace hyperion // find first Y pixel of the image for (int y = 0; y < height33percent; ++y) { - const Pixel_T & color1 = image(x, y );// left side top check - const Pixel_T & color2 = image(x, (height - y)); // left side bottom check - const Pixel_T & color3 = image( (width - x), y); // right side top check - const Pixel_T & color4 = image( (width - x), (height - y)); // right side bottom check - if (!isBlack(color1) || !isBlack(color2) || !isBlack(color3) || !isBlack(color4)) + // left side top + left side bottom + right side top + right side bottom + if (!isBlack(image(x, y)) + || !isBlack(image(x, (height - y))) + || !isBlack(image((width - x), y)) + || !isBlack(image((width - x), (height - y)))) { -// std::cout << "y " << y << " lt " << int(isBlack(color1)) << " lb " << int(isBlack(color2)) << " rt " << int(isBlack(color3)) << " rb " << int(isBlack(color4)) << std::endl; +// std::cout << "y " << y << " lt " << int(isBlack(color1)) << " lb " << int(isBlack(color2)) << " rt " << int(isBlack(color3)) << " rb " << int(isBlack(color4)) << std::endl; firstNonBlackYPixelIndex = y; break; } diff --git a/include/blackborder/BlackBorderProcessor.h b/include/blackborder/BlackBorderProcessor.h index d5f1b9de..0b625f08 100644 --- a/include/blackborder/BlackBorderProcessor.h +++ b/include/blackborder/BlackBorderProcessor.h @@ -4,30 +4,28 @@ // QT includes #include +// util +#include +#include +#include + // Local Hyperion includes #include "BlackBorderDetector.h" +class Hyperion; + namespace hyperion { /// /// The BlackBorder processor is a wrapper around the black-border detector for keeping track of /// detected borders and count of the type and size of detected borders. /// - class BlackBorderProcessor + class BlackBorderProcessor : public QObject { + Q_OBJECT public: - /// - /// Constructor for the BlackBorderProcessor - /// @param unknownFrameCnt The number of frames(images) that need to contain an unknown - /// border before the current border is set to unknown - /// @param borderFrameCnt The number of frames(images) that need to contain a vertical or - /// horizontal border becomes the current border - /// @param blurRemoveCnt The size to add to a horizontal or vertical border (because the - /// outer pixels is blurred (black and color combined due to image scaling)) - /// @param[in] blackborderThreshold The threshold which the blackborder detector should use - /// - BlackBorderProcessor(const QJsonObject &blackborderConfig); - + BlackBorderProcessor(Hyperion* hyperion, QObject* parent); + ~BlackBorderProcessor(); /// /// Return the current (detected) border /// @return The current border @@ -46,6 +44,13 @@ namespace hyperion /// void setEnabled(bool enable); + /// + /// Sets the _hardDisabled state, if True prevents the enable from COMP_BLACKBORDER state emit (mimiks wrong state to external!) + /// It's not possible to enable bb from this method, if the user requsted a disable! + /// @param disable The new state + /// + void setHardDisable(const bool& disable); + /// /// Processes the image. This performs detecion of black-border on the given image and /// updates the current border accordingly. If the current border is updated the method call @@ -70,11 +75,11 @@ namespace hyperion } if (_detectionMode == "default") { - imageBorder = _detector.process(image); + imageBorder = _detector->process(image); } else if (_detectionMode == "classic") { - imageBorder = _detector.process_classic(image); + imageBorder = _detector->process_classic(image); } else if (_detectionMode == "osd") { - imageBorder = _detector.process_osd(image); + imageBorder = _detector->process_osd(image); } // add blur to the border if (imageBorder.horizontalSize > 0) @@ -89,8 +94,23 @@ namespace hyperion const bool borderUpdated = updateBorder(imageBorder); return borderUpdated; } + private slots: + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + + /// + /// @brief Handle component state changes, it's not possible for BB to be enabled, when a hardDisable is active + /// + void componentStateChanged(const hyperion::Components component, bool enable); private: + /// Hyperion instance + Hyperion* _hyperion; + /// /// Updates the current border based on the newly detected border. Returns true if the /// current border has changed. @@ -102,24 +122,24 @@ namespace hyperion /// flag for blackborder detector usage bool _enabled; - + /// The number of unknown-borders detected before it becomes the current border - const unsigned _unknownSwitchCnt; + unsigned _unknownSwitchCnt; /// The number of horizontal/vertical borders detected before it becomes the current border - const unsigned _borderSwitchCnt; + unsigned _borderSwitchCnt; // The number of frames that are "ignored" before a new border gets set as _previousDetectedBorder - const unsigned _maxInconsistentCnt; + unsigned _maxInconsistentCnt; /// The number of pixels to increase a detected border for removing blury pixels unsigned _blurRemoveCnt; /// The border detection mode - const QString _detectionMode; + QString _detectionMode; /// The blackborder detector - BlackBorderDetector _detector; + BlackBorderDetector* _detector; /// The current detected border BlackBorder _currentBorder; @@ -131,5 +151,12 @@ namespace hyperion unsigned _consistentCnt; /// The number of frame the previous detected border NOT matched the incomming border unsigned _inconsistentCnt; + /// old threshold + double _oldThreshold; + /// True when disabled in specific situations, this prevents to enable BB when the visible priority requested a disable + bool _hardDisabled; + /// Reflect the last component state request from user (comp change) + bool _userEnabled; + }; } // end namespace hyperion diff --git a/include/boblightserver/BoblightServer.h b/include/boblightserver/BoblightServer.h index 42ed587c..d0e1567b 100644 --- a/include/boblightserver/BoblightServer.h +++ b/include/boblightserver/BoblightServer.h @@ -4,15 +4,19 @@ #include // Qt includes -#include #include +#include // Hyperion includes -#include #include #include +// settings +#include + class BoblightClientConnection; +class Hyperion; +class QTcpServer; /// /// This class creates a TCP server which accepts connections from boblight clients. @@ -27,25 +31,24 @@ public: /// @param hyperion Hyperion instance /// @param port port number on which to start listening for connections /// - BoblightServer(const int priority, uint16_t port = 19333); + BoblightServer(Hyperion* hyperion, const QJsonDocument& config); ~BoblightServer(); /// /// @return the port number on which this TCP listens for incoming connections /// uint16_t getPort() const; - + /// @return true if server is active (bind to a port) /// - bool active() { return _isActive; }; - bool componentState() { return active(); }; + bool active(); public slots: /// /// bind server to network /// void start(); - + /// /// close server /// @@ -53,8 +56,12 @@ public slots: void componentStateChanged(const hyperion::Components component, bool enable); -signals: - void statusChanged(bool isActive); + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); private slots: /// @@ -73,19 +80,17 @@ private: Hyperion * _hyperion; /// The TCP server object - QTcpServer _server; + QTcpServer * _server; /// List with open connections QSet _openConnections; /// hyperion priority - const int _priority; + int _priority; /// Logger instance Logger * _log; - /// state of connection - bool _isActive; - + // current port uint16_t _port; }; diff --git a/include/bonjour/bonjourbrowserwrapper.h b/include/bonjour/bonjourbrowserwrapper.h new file mode 100644 index 00000000..527576d4 --- /dev/null +++ b/include/bonjour/bonjourbrowserwrapper.h @@ -0,0 +1,68 @@ +#pragma once +// qt incl +#include +#include +#include + +#include + +class BonjourServiceBrowser; +class BonjourServiceResolver; +class QTimer; + +class BonjourBrowserWrapper : public QObject +{ + Q_OBJECT +private: + friend class HyperionDaemon; + /// + /// @brief Browse for hyperion services in bonjour, constructed from HyperionDaemon + /// Searching for hyperion http service by default + /// + BonjourBrowserWrapper(QObject * parent = 0); + +public: + + /// + /// @brief Browse for a service + /// + bool browseForServiceType(const QString &serviceType); + /// + /// @brief Get all available sessions + /// + QMap getAllServices() { return _hyperionSessions; }; + + static BonjourBrowserWrapper* instance; + static BonjourBrowserWrapper* getInstance(){ return instance; }; + +signals: + /// + /// @brief Emits whenever a change happend + /// + void browserChange(const QMap& bRegisters); + +private: + /// map of service names and browsers + QMap< QString, BonjourServiceBrowser* > _browsedServices; + /// Resolver + BonjourServiceResolver* _bonjourResolver; + + // contains all current active service sessions + QMap _hyperionSessions; + + QString _bonjourCurrentServiceToResolve; + /// timer to resolve changes + QTimer* _timerBonjourResolver; + +private slots: + /// + /// @brief is called whenever a BonjourServiceBrowser emits change + void currentBonjourRecordsChanged(const QList &list); + /// @brief new record resolved + void bonjourRecordResolved(const QHostInfo &hostInfo, int port); + + /// + /// @brief timer slot which updates regularly entries + /// + void bonjourResolve(); +}; diff --git a/include/bonjour/bonjourserviceregister.h b/include/bonjour/bonjourserviceregister.h index 137f97cf..d0c65d87 100755 --- a/include/bonjour/bonjourserviceregister.h +++ b/include/bonjour/bonjourserviceregister.h @@ -43,9 +43,12 @@ public: BonjourServiceRegister(QObject *parent = 0); ~BonjourServiceRegister(); - void registerService(const BonjourRecord &record, quint16 servicePort, std::vector> txt); + void registerService(const QString& service, const int& port); + void registerService(const BonjourRecord &record, quint16 servicePort, std::vector> txt = std::vector>()); inline BonjourRecord registeredRecord() const {return finalRecord; } + const quint16 & getPort() { return _port; }; + signals: void error(DNSServiceErrorType error); void serviceRegistered(const BonjourRecord &record); @@ -61,6 +64,9 @@ private: DNSServiceRef dnssref; QSocketNotifier *bonjourSocket; BonjourRecord finalRecord; + + // current port + quint16 _port = 0; }; #endif // BONJOURSERVICEREGISTER_H diff --git a/include/effectengine/Effect.h b/include/effectengine/Effect.h new file mode 100644 index 00000000..3e786284 --- /dev/null +++ b/include/effectengine/Effect.h @@ -0,0 +1,97 @@ +#pragma once + +// Python includes +// collide of qt slots macro +#undef slots +#include "Python.h" +#define slots + +// Qt includes +#include +#include +#include +#include +#include +#include + +// Hyperion includes +#include +#include + +class Hyperion; +class Logger; + +class Effect : public QThread +{ + Q_OBJECT + +public: + friend class EffectModule; + + Effect(Hyperion *hyperion + , int priority + , int timeout + , const QString &script + , const QString &name + , const QJsonObject &args = QJsonObject() + , const QString &imageData = "" + ); + virtual ~Effect(); + + virtual void run(); + + int getPriority() const { return _priority; }; + + /// + /// @brief Set manual interuption to true, + /// Note: DO NOT USE QThread::interuption! + /// + void setInteruptionFlag() { _interupt = true; }; + + /// + /// @brief Check if the interuption flag has been set + /// @return The flag state + /// + bool hasInteruptionFlag() { return _interupt; }; + + QString getScript() const { return _script; } + QString getName() const { return _name; } + + int getTimeout() const {return _timeout; } + + QJsonObject getArgs() const { return _args; } + +signals: + void setInput(const int priority, const std::vector &ledColors, const int timeout_ms, const bool &clearEffect); + void setInputImage(const int priority, const Image &image, const int timeout_ms, const bool &clearEffect); + +private: + + void addImage(); + + Hyperion *_hyperion; + + const int _priority; + + const int _timeout; + + const QString _script; + const QString _name; + + const QJsonObject _args; + const QString _imageData; + + int64_t _endTime; + + /// Buffer for colorData + QVector _colors; + + Logger *_log; + // Reflects whenever this effects should interupt (timeout or external request) + bool _interupt = false; + + QSize _imageSize; + QImage _image; + QPainter *_painter; + QVector _imageStack; +}; diff --git a/include/effectengine/EffectEngine.h b/include/effectengine/EffectEngine.h index 323b8270..8ee1ba87 100644 --- a/include/effectengine/EffectEngine.h +++ b/include/effectengine/EffectEngine.h @@ -17,38 +17,72 @@ #include #include -// pre-declarioation +// pre-declaration class Effect; -typedef struct _ts PyThreadState; +class EffectFileHandler; class EffectEngine : public QObject { Q_OBJECT public: - EffectEngine(Hyperion * hyperion, const QJsonObject & jsonEffectConfig); + EffectEngine(Hyperion * hyperion); virtual ~EffectEngine(); - void readEffects(); - - const std::list & getEffects() const - { - return _availableEffects; - }; + const std::list & getEffects() const { return _availableEffects; }; const std::list & getActiveEffects(); - const std::list & getEffectSchemas() - { - return _effectSchemas; - }; + /// + /// Get available schemas from EffectFileHandler + /// @return all schemas + /// + const std::list & getEffectSchemas(); + + /// + /// @brief Save an effect with EffectFileHandler + /// @param obj The effect args + /// @param[out] resultMsg The feedback message + /// @return True on success else false + /// + const bool saveEffect(const QJsonObject& obj, QString& resultMsg); + + /// + /// @brief Delete an effect by name. + /// @param[in] effectName The effect name to delete + /// @param[out] resultMsg The message on error + /// @return True on success else false + /// + const bool deleteEffect(const QString& effectName, QString& resultMsg); + + /// + /// @brief Get all init data of the running effects and stop them + /// + void cacheRunningEffects(); + + /// + /// @brief Start all cached effects, origin and smooth cfg is default + /// + void startCachedEffects(); + +signals: + /// Emit when the effect list has been updated + void effectListUpdated(); public slots: /// Run the specified effect on the given priority channel and optionally specify a timeout int runEffect(const QString &effectName, int priority, int timeout = -1, const QString &origin="System"); /// Run the specified effect on the given priority channel and optionally specify a timeout - int runEffect(const QString &effectName, const QJsonObject & args, int priority, int timeout = -1, const QString &pythonScript = "", const QString &origin = "System", unsigned smoothCfg=0); + int runEffect(const QString &effectName + , const QJsonObject &args + , int priority + , int timeout = -1 + , const QString &pythonScript = "" + , const QString &origin = "System" + , unsigned smoothCfg=0 + , const QString &imageData = "" + ); /// Clear any effect running on the provided channel void channelCleared(int priority); @@ -59,28 +93,36 @@ public slots: private slots: void effectFinished(); + /// + /// @brief is called whenever the EffectFileHandler emits updated effect list + /// + void handleUpdatedEffectList(); + private: - bool loadEffectDefinition(const QString & path, const QString & effectConfigFile, EffectDefinition &effectDefinition); - - bool loadEffectSchema(const QString & path, const QString & effectSchemaFile, EffectSchema &effectSchema); - /// Run the specified effect on the given priority channel and optionally specify a timeout - int runEffectScript(const QString &script, const QString &name, const QJsonObject & args, int priority, int timeout = -1, const QString & origin="System", unsigned smoothCfg=0); + int runEffectScript(const QString &script + ,const QString &name + , const QJsonObject &args + , int priority + , int timeout = -1 + , const QString &origin="System" + , unsigned smoothCfg=0 + , const QString &imageData = "" + ); private: Hyperion * _hyperion; - QJsonObject _effectConfig; - std::list _availableEffects; std::list _activeEffects; std::list _availableActiveEffects; - std::list _effectSchemas; + std::list _cachedActiveEffects; Logger * _log; - PyThreadState* _mainThreadState; + // The global effect file handler + EffectFileHandler* _effectFileHandler; }; diff --git a/include/effectengine/EffectFileHandler.h b/include/effectengine/EffectFileHandler.h new file mode 100644 index 00000000..cdcc76d9 --- /dev/null +++ b/include/effectengine/EffectFileHandler.h @@ -0,0 +1,86 @@ +#pragma once + +// util +#include +#include +#include +#include + +class EffectFileHandler : public QObject +{ + Q_OBJECT +private: + friend class HyperionDaemon; + EffectFileHandler(const QString& rootPath, const QJsonDocument& effectConfig, QObject* parent = nullptr); + +public: + static EffectFileHandler* efhInstance; + static EffectFileHandler* getInstance() { return efhInstance; }; + + /// + /// @brief Get all available effects + /// + const std::list & getEffects() const { return _availableEffects; }; + + /// + /// @brief Get all available schemas + /// + const std::list & getEffectSchemas() { return _effectSchemas; }; + + /// + /// @brief Save an effect + /// @param obj The effect args + /// @param[out] resultMsg The feedback message + /// @return True on success else false + /// + const bool saveEffect(const QJsonObject& obj, QString& resultMsg); + + /// + /// @brief Delete an effect by name. + /// @param[in] effectName The effect name to delete + /// @param[out] resultMsg The message on error + /// @return True on success else false + /// + const bool deleteEffect(const QString& effectName, QString& resultMsg); + +public slots: + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + +signals: + /// + /// @brief Emits whenever the data changes for an effect + /// + void effectListChanged(); + +private: + /// + /// @brief refresh available schemas and effects + /// + void updateEffects(); + + /// + /// @brief Load the effect definition, called by updateEffects() + /// + bool loadEffectDefinition(const QString & path, const QString & effectConfigFile, EffectDefinition &effectDefinition); + + /// + /// @brief load effect schemas, called by updateEffects() + /// + bool loadEffectSchema(const QString & path, const QString & effectSchemaFile, EffectSchema &effectSchema); + +private: + QJsonObject _effectConfig; + Logger* _log; + const QString _rootPath; + + // available effects + std::list _availableEffects; + + // all schemas + std::list _effectSchemas; +}; diff --git a/libsrc/effectengine/Effect.h b/include/effectengine/EffectModule.h similarity index 55% rename from libsrc/effectengine/Effect.h rename to include/effectengine/EffectModule.h index 7d51d5a5..c274d05b 100644 --- a/libsrc/effectengine/Effect.h +++ b/include/effectengine/EffectModule.h @@ -1,49 +1,27 @@ #pragma once -// Python includes -// collide of qt slots macro #undef slots #include #define slots -// Qt includes -#include -#include -#include -#include -#include +#include -// Hyperion includes -#include -#include +class Effect; -class Effect : public QThread +class EffectModule { - Q_OBJECT - public: - Effect(PyThreadState* mainThreadState, int priority, int timeout, const QString & script, const QString & name, const QJsonObject & args = QJsonObject(), const QString & origin="System", unsigned smoothCfg=0); - virtual ~Effect(); + // Python 3 module def + static struct PyModuleDef moduleDef; - virtual void run(); + // Init module + static PyObject* PyInit_hyperion(); - int getPriority() const { return _priority; }; + // Register module once + static void registerHyperionExtensionModule(); - QString getScript() const { return _script; } - QString getName() const { return _name; } - - int getTimeout() const {return _timeout; } - - QJsonObject getArgs() const { return _args; } - - /// This function registers the extension module in Python - static void registerHyperionExtensionModule(); - -signals: - void setColors(int priority, const std::vector &ledColors, const int timeout_ms, bool clearEffects, hyperion::Components componentconst, QString origin, unsigned smoothCfg); - -private: - PyObject * json2python(const QJsonValue & jsonData) const; + // json 2 python + static PyObject * json2python(const QJsonValue & jsonData); // Wrapper methods for Python interpreter extra buildin methods static PyMethodDef effectMethods[]; @@ -71,38 +49,5 @@ private: static PyObject* wrapImageCOffset (PyObject *self, PyObject *args); static PyObject* wrapImageCShear (PyObject *self, PyObject *args); static PyObject* wrapImageResetT (PyObject *self, PyObject *args); - static Effect * getEffect(); - - static struct PyModuleDef moduleDef; - static PyObject* PyInit_hyperion(); - - void addImage(); - - PyThreadState* _mainThreadState; - - const int _priority; - - const int _timeout; - - const QString _script; - const QString _name; - unsigned _smoothCfg; - - const QJsonObject _args; - - int64_t _endTime; - - /// The processor for translating images to led-values - ImageProcessor * _imageProcessor; - - /// Buffer for colorData - QVector _colors; - - Logger* _log; - - QString _origin; - QSize _imageSize; - QImage _image; - QPainter* _painter; - QVector _imageStack; + static Effect * getEffect(); }; diff --git a/include/flatbufserver/FlatBufferConnection.h b/include/flatbufserver/FlatBufferConnection.h new file mode 100644 index 00000000..e0259548 --- /dev/null +++ b/include/flatbufserver/FlatBufferConnection.h @@ -0,0 +1,134 @@ +#pragma once + +// Qt includes +#include +#include +#include +#include +#include +#include + +// hyperion util +#include +#include +#include +#include + +// flatbuffer FBS +#include "hyperion_reply_generated.h" +#include "hyperion_request_generated.h" + +/// +/// Connection class to setup an connection to the hyperion server and execute commands. +/// +class FlatBufferConnection : public QObject +{ + + Q_OBJECT + +public: + /// + /// @brief Constructor + /// @param address The address of the Hyperion server (for example "192.168.0.32:19444) + /// @param skipReply If true skip reply + /// + FlatBufferConnection(const QString& origin, const QString & address, const int& priority, const bool& skipReply); + + /// + /// @brief Destructor + /// + ~FlatBufferConnection(); + + /// @brief Do not read reply messages from Hyperion if set to true + void setSkipReply(const bool& skip); + + /// + /// @brief Register a new priority with given origin + /// @param origin The user friendly origin string + /// @param priority The priority to register + /// + void setRegister(const QString& origin, int priority); + + /// + /// @brief Set all leds to the specified color + /// @param color The color + /// @param priority The priority + /// @param duration The duration in milliseconds + /// + void setColor(const ColorRgb & color, int priority, int duration = 1); + + /// + /// @brief Clear the given priority channel + /// @param priority The priority + /// + void clear(int priority); + + /// + /// @brief Clear all priority channels + /// + void clearAll(); + + /// + /// @brief Send a command message and receive its reply + /// @param message The message to send + /// + void sendMessage(const uint8_t* buffer, uint32_t size); + +public slots: + /// + /// @brief Set the leds according to the given image + /// @param image The image + /// + void setImage(const Image &image); + +private slots: + /// + /// @brief Try to connect to the Hyperion host + /// + void connectToHost(); + + /// + /// @brief Slot called when new data has arrived + /// + void readData(); + +signals: + + /// + /// @brief emits when a new videoMode was requested from flatbuf client + /// + void setVideoMode(const VideoMode videoMode); + +private: + + /// + /// @brief Parse a reply message + /// @param reply The received reply + /// @return true if the reply indicates success + /// + bool parseReply(const hyperionnet::Reply *reply); + +private: + /// The TCP-Socket with the connection to the server + QTcpSocket _socket; + + QString _origin; + int _priority; + + /// Host address + QString _host; + + /// Host port + uint16_t _port; + + /// buffer for reply + QByteArray _receiveBuffer; + + QTimer _timer; + QAbstractSocket::SocketState _prevSocketState; + + Logger * _log; + flatbuffers::FlatBufferBuilder _builder; + + bool _registered; +}; diff --git a/include/flatbufserver/FlatBufferServer.h b/include/flatbufserver/FlatBufferServer.h new file mode 100644 index 00000000..5f01b7cf --- /dev/null +++ b/include/flatbufserver/FlatBufferServer.h @@ -0,0 +1,65 @@ +#pragma once + +// util +#include +#include + +// qt +#include + +class QTcpServer; +class FlatBufferClient; + +/// +/// @brief A TcpServer to receive images of different formats with Google Flatbuffer +/// Images will be forwarded to all Hyperion instances +/// +class FlatBufferServer : public QObject +{ + Q_OBJECT +public: + FlatBufferServer(const QJsonDocument& config, QObject* parent = nullptr); + ~FlatBufferServer(); + +public slots: + /// + /// @brief Handle settings update + /// @param type The type from enum + /// @param config The configuration + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + + void initServer(); + +private slots: + /// + /// @brief Is called whenever a new socket wants to connect + /// + void newConnection(); + + /// + /// @brief is called whenever a client disconnected + /// + void clientDisconnected(); + +private: + /// + /// @brief Start the server with current _port + /// + void startServer(); + + /// + /// @brief Stop server + /// + void stopServer(); + + +private: + QTcpServer* _server; + Logger* _log; + int _timeout; + quint16 _port; + const QJsonDocument _config; + + QVector _openConnections; +}; diff --git a/include/grabber/AmlogicWrapper.h b/include/grabber/AmlogicWrapper.h index bfcc9552..0f241162 100644 --- a/include/grabber/AmlogicWrapper.h +++ b/include/grabber/AmlogicWrapper.h @@ -18,9 +18,8 @@ public: /// @param[in] grabWidth The width of the grabbed image [pixels] /// @param[in] grabHeight The height of the grabbed images [pixels] /// @param[in] updateRate_Hz The image grab rate [Hz] - /// @param[in] hyperion The instance of Hyperion used to write the led values /// - AmlogicWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority); + AmlogicWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz); /// /// Destructor of this dispmanx frame grabber. Releases any claimed resources. diff --git a/include/grabber/DispmanxFrameGrabber.h b/include/grabber/DispmanxFrameGrabber.h index 3d22d3dc..82b14b35 100644 --- a/include/grabber/DispmanxFrameGrabber.h +++ b/include/grabber/DispmanxFrameGrabber.h @@ -40,14 +40,23 @@ public: /// int grabFrame(Image & image); + /// + ///@brief Set new width and height for dispmanx, overwrite Grabber.h impl + virtual bool setWidthHeight(int width, int height); + private: - /// + /// /// Updates the frame-grab flags as used by the VC library for frame grabbing /// /// @param vc_flags The snapshot grabbing mask /// void setFlags(const int vc_flags); - + + /// + /// @brief free _vc_resource and captureBuffer + /// + void freeResources(); + /// Handle to the display that is being captured DISPMANX_DISPLAY_HANDLE_T _vc_display; diff --git a/include/grabber/DispmanxWrapper.h b/include/grabber/DispmanxWrapper.h index 618a8b27..e231c809 100644 --- a/include/grabber/DispmanxWrapper.h +++ b/include/grabber/DispmanxWrapper.h @@ -7,8 +7,7 @@ /// /// The DispmanxWrapper uses an instance of the DispmanxFrameGrabber to obtain ImageRgb's from the -/// displayed content. This ImageRgb is processed to a ColorRgb for each led and commmited to the -/// attached Hyperion. +/// displayed content. This ImageRgb is forwarded to all Hyperion instances via HyperionDaemon /// class DispmanxWrapper: public GrabberWrapper { @@ -20,9 +19,8 @@ public: /// @param[in] grabWidth The width of the grabbed image [pixels] /// @param[in] grabHeight The height of the grabbed images [pixels] /// @param[in] updateRate_Hz The image grab rate [Hz] - /// @param[in] hyperion The instance of Hyperion used to write the led values /// - DispmanxWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority); + DispmanxWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz); /// /// Destructor of this dispmanx frame grabber. Releases any claimed resources. diff --git a/include/grabber/FramebufferFrameGrabber.h b/include/grabber/FramebufferFrameGrabber.h index 6077bb1b..6296755f 100644 --- a/include/grabber/FramebufferFrameGrabber.h +++ b/include/grabber/FramebufferFrameGrabber.h @@ -5,7 +5,7 @@ #include /// -/// The FramebufferFrameGrabber is used for creating snapshots of the display (screenshots) +/// The FramebufferFrameGrabber is used for creating snapshots of the display (screenshots) /// class FramebufferFrameGrabber : public Grabber { @@ -30,13 +30,18 @@ public: /// int grabFrame(Image & image); + /// + /// @brief Overwrite Grabber.h implememtation + /// + virtual void setDevicePath(const QString& path); + private: /// Framebuffer file descriptor int _fbfd; /// Pointer to framebuffer unsigned char * _fbp; - + /// Framebuffer device e.g. /dev/fb0 - const QString _fbDevice; + QString _fbDevice; }; diff --git a/include/grabber/FramebufferWrapper.h b/include/grabber/FramebufferWrapper.h index 164c56b3..a7c9243c 100644 --- a/include/grabber/FramebufferWrapper.h +++ b/include/grabber/FramebufferWrapper.h @@ -20,7 +20,7 @@ public: /// @param[in] grabHeight The height of the grabbed images [pixels] /// @param[in] updateRate_Hz The image grab rate [Hz] /// - FramebufferWrapper(const QString & device, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority); + FramebufferWrapper(const QString & device, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz); /// /// Destructor of this framebuffer frame grabber. Releases any claimed resources. diff --git a/include/grabber/OsxFrameGrabber.h b/include/grabber/OsxFrameGrabber.h index 8cba6394..59e39930 100644 --- a/include/grabber/OsxFrameGrabber.h +++ b/include/grabber/OsxFrameGrabber.h @@ -12,7 +12,7 @@ #include /// -/// The OsxFrameGrabber is used for creating snapshots of the display (screenshots) +/// The OsxFrameGrabber is used for creating snapshots of the display (screenshots) /// class OsxFrameGrabber : public Grabber { @@ -37,10 +37,15 @@ public: /// int grabFrame(Image & image); -private: + /// + /// @brief Overwrite Grabber.h implementation + /// + virtual void setDisplayIndex(int index); + +private: /// display - const unsigned _screenIndex; - + unsigned _screenIndex; + /// Reference to the captured diaplay CGDirectDisplayID _display; }; diff --git a/include/grabber/OsxWrapper.h b/include/grabber/OsxWrapper.h index 65b765d7..65ce70ab 100644 --- a/include/grabber/OsxWrapper.h +++ b/include/grabber/OsxWrapper.h @@ -19,9 +19,8 @@ public: /// @param[in] grabWidth The width of the grabbed image [pixels] /// @param[in] grabHeight The height of the grabbed images [pixels] /// @param[in] updateRate_Hz The image grab rate [Hz] - /// @param[in] hyperion The instance of Hyperion used to write the led values /// - OsxWrapper(const unsigned display, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority); + OsxWrapper(const unsigned display, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz); /// /// Destructor of this osx frame grabber. Releases any claimed resources. diff --git a/include/grabber/QtGrabber.h b/include/grabber/QtGrabber.h new file mode 100644 index 00000000..288728c4 --- /dev/null +++ b/include/grabber/QtGrabber.h @@ -0,0 +1,96 @@ +#pragma once + +#include + +// Hyperion-utils includes +#include +#include + +class QScreen; + +/// +/// @brief The platform capture implementation based on QT API +/// +class QtGrabber : public Grabber +{ +public: + + QtGrabber(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation, int display); + + virtual ~QtGrabber(); + + /// + /// Captures a single snapshot of the display and writes the data to the given image. The + /// provided image should have the same dimensions as the configured values (_width and + /// _height) + /// + /// @param[out] image The snapped screenshot (should be initialized with correct width and + /// height) + /// + virtual int grabFrame(Image & image); + + /// + /// @brief Set a new video mode + /// + virtual void setVideoMode(VideoMode mode); + + /// + /// @brief Apply new width/height values, overwrite Grabber.h implementation as qt doesn't use width/height, just pixelDecimation to calc dimensions + /// + virtual bool setWidthHeight(int width, int height) { return true; }; + + /// + /// @brief Apply new pixelDecimation + /// + virtual void setPixelDecimation(int pixelDecimation); + + /// + /// Set the crop values + /// @param cropLeft Left pixel crop + /// @param cropRight Right pixel crop + /// @param cropTop Top pixel crop + /// @param cropBottom Bottom pixel crop + /// + virtual void setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom); + + /// + /// @brief Apply display index + /// + virtual void setDisplayIndex(int index); + +private slots: + /// + /// @brief is called whenever the current _screen changes it's geometry + /// @param geo The new geometry + /// + void geometryChanged(const QRect &geo); + +private: + /// + /// @brief Setup a new capture display, will free the previous one + /// @return True on success, false if no display is found + /// + const bool setupDisplay(); + + /// + /// @brief Is called whenever we need new screen dimension calculations based on window geometry + /// + int updateScreenDimensions(const bool& force); + + /// + /// @brief free the _screen pointer + /// + void freeResources(); + +private: + + unsigned _display; + int _pixelDecimation; + unsigned _screenWidth; + unsigned _screenHeight; + unsigned _src_x; + unsigned _src_y; + unsigned _src_x_max; + unsigned _src_y_max; + QScreen* _screen; +}; diff --git a/include/grabber/QtWrapper.h b/include/grabber/QtWrapper.h new file mode 100644 index 00000000..3c4b7d28 --- /dev/null +++ b/include/grabber/QtWrapper.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +/// +/// The QtWrapper uses QtFramework API's to get a picture from system +/// +class QtWrapper: public GrabberWrapper +{ +public: + /// + /// Constructs the framebuffer frame grabber with a specified grab size and update rate. + /// + /// @param[in] cropLeft Remove from left [pixels] + /// @param[in] cropRight Remove from right [pixels] + /// @param[in] cropTop Remove from top [pixels] + /// @param[in] cropBottom Remove from bottom [pixels] + /// @param[in] pixelDecimation Decimation factor for image [pixels] + /// @param[in] updateRate_Hz The image grab rate [Hz] + /// + QtWrapper(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation, int display, const unsigned updateRate_Hz); + + /// + /// Destructor of this qt frame grabber. Releases any claimed resources. + /// + virtual ~QtWrapper() {}; + +public slots: + /// + /// Performs a single frame grab and computes the led-colors + /// + virtual void action(); + +private: + /// The actual grabber + QtGrabber _grabber; +}; diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/V4L2Grabber.h index b21aa005..0d9409a3 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/V4L2Grabber.h @@ -23,13 +23,9 @@ class V4L2Grabber : public Grabber public: V4L2Grabber(const QString & device, - int input, - VideoStandard videoStandard, PixelFormat pixelFormat, - unsigned width, - unsigned height, - int frameDecimation, - int horizontalPixelDecimation, - int verticalPixelDecimation + VideoStandard videoStandard, + PixelFormat pixelFormat, + int pixelDecimation ); virtual ~V4L2Grabber(); @@ -37,21 +33,46 @@ public: bool getSignalDetectionEnabled(); int grabFrame(Image &); - -public slots: - void setSignalThreshold( + + /// + /// @brief overwrite Grabber.h implementation, as v4l doesn't use width/height + /// + virtual void setWidthHeight(){}; + + /// + /// @brief set new PixelDecimation value to ImageResampler + /// @param pixelDecimation The new pixelDecimation value + /// + virtual void setPixelDecimation(int pixelDecimation); + + /// + /// @brief overwrite Grabber.h implementation + /// + virtual void setSignalThreshold( double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold, - int noSignalCounterThreshold); + int noSignalCounterThreshold = 50); - void setSignalDetectionOffset( + /// + /// @brief overwrite Grabber.h implementation + /// + virtual void setSignalDetectionOffset( double verticalMin, double horizontalMin, double verticalMax, double horizontalMax); + /// + /// @brief overwrite Grabber.h implementation + /// + virtual void setSignalDetectionEnable(bool enable); - void setSignalDetectionEnable(bool enable); + /// + /// @brief overwrite Grabber.h implementation + /// + virtual void setDeviceVideoStandard(QString device, VideoStandard videoStandard); + +public slots: bool start(); @@ -66,7 +87,7 @@ private slots: private: void getV4Ldevices(); - + bool init(); void uninit(); @@ -120,9 +141,9 @@ private: std::vector _buffers; PixelFormat _pixelFormat; + int _pixelDecimation; int _lineLength; int _frameByteSize; - int _frameDecimation; // signal detection int _noSignalCounterThreshold; @@ -134,7 +155,6 @@ private: double _y_frac_min; double _x_frac_max; double _y_frac_max; - int _currentFrame; QSocketNotifier * _streamNotifier; diff --git a/include/grabber/V4L2Wrapper.h b/include/grabber/V4L2Wrapper.h index ffc78a7c..e013e0ef 100644 --- a/include/grabber/V4L2Wrapper.h +++ b/include/grabber/V4L2Wrapper.h @@ -9,17 +9,9 @@ class V4L2Wrapper : public GrabberWrapper public: V4L2Wrapper(const QString & device, - int input, VideoStandard videoStandard, PixelFormat pixelFormat, - unsigned width, - unsigned height, - int frameDecimation, - int pixelDecimation, - double redSignalThreshold, - double greenSignalThreshold, - double blueSignalThreshold, - const int priority); + int pixelDecimation ); virtual ~V4L2Wrapper() {}; bool getSignalDetectionEnable(); @@ -28,19 +20,16 @@ public slots: bool start(); void stop(); + void setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold); void setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom); void setSignalDetectionOffset(double verticalMin, double horizontalMin, double verticalMax, double horizontalMax); void setSignalDetectionEnable(bool enable); -// signals: -// void emitColors(int priority, const std::vector &ledColors, const int timeout_ms); - private slots: void newFrame(const Image & image); void readError(const char* err); virtual void action(); - void checkSources(); private: /// The V4L2 grabber diff --git a/include/grabber/X11Grabber.h b/include/grabber/X11Grabber.h index 7ef286fb..ab7adf45 100755 --- a/include/grabber/X11Grabber.h +++ b/include/grabber/X11Grabber.h @@ -17,12 +17,12 @@ class X11Grabber : public Grabber { public: - X11Grabber(bool useXGetImage, int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation); + X11Grabber(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation); virtual ~X11Grabber(); bool Setup(); - + /// /// Captures a single snapshot of the display and writes the data to the given image. The /// provided image should have the same dimensions as the configured values (_width and @@ -32,15 +32,34 @@ public: /// height) /// virtual int grabFrame(Image & image, bool forceUpdate=false); - + /// /// update dimension according current screen int updateScreenDimensions(bool force=false); virtual void setVideoMode(VideoMode mode); + /// + /// @brief Apply new width/height values, overwrite Grabber.h implementation as X11 doesn't use width/height, just pixelDecimation to calc dimensions + /// + virtual bool setWidthHeight(int width, int height) { return true; }; + + /// + /// @brief Apply new pixelDecimation + /// + virtual void setPixelDecimation(int pixelDecimation); + + /// + /// Set the crop values + /// @param cropLeft Left pixel crop + /// @param cropRight Right pixel crop + /// @param cropTop Top pixel crop + /// @param cropBottom Bottom pixel crop + /// + virtual void setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom); + private: - bool _useXGetImage, _XShmAvailable, _XShmPixmapAvailable, _XRenderAvailable; + bool _XShmAvailable, _XShmPixmapAvailable, _XRenderAvailable; XImage* _xImage; XShmSegmentInfo _shminfo; @@ -49,17 +68,16 @@ private: Display* _x11Display; Window _window; XWindowAttributes _windowAttr; - + Pixmap _pixmap; XRenderPictFormat* _srcFormat; XRenderPictFormat* _dstFormat; XRenderPictureAttributes _pictAttr; Picture _srcPicture; Picture _dstPicture; - + XTransform _transform; - int _horizontalDecimation; - int _verticalDecimation; + int _pixelDecimation; unsigned _screenWidth; unsigned _screenHeight; @@ -67,7 +85,7 @@ private: unsigned _src_y; Image _image; - + void freeResources(); void setupResources(); }; diff --git a/include/grabber/X11Wrapper.h b/include/grabber/X11Wrapper.h index e29b3ca4..51f2c77c 100644 --- a/include/grabber/X11Wrapper.h +++ b/include/grabber/X11Wrapper.h @@ -24,7 +24,7 @@ public: /// @param[in] grabHeight The height of the grabbed images [pixels] /// @param[in] updateRate_Hz The image grab rate [Hz] /// - X11Wrapper(bool useXGetImage, int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation, const unsigned updateRate_Hz, const int priority); + X11Wrapper(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation, const unsigned updateRate_Hz); /// /// Destructor of this framebuffer frame grabber. Releases any claimed resources. @@ -43,4 +43,3 @@ private: bool _init; }; - diff --git a/include/hyperion/BGEffectHandler.h b/include/hyperion/BGEffectHandler.h new file mode 100644 index 00000000..3ca229dc --- /dev/null +++ b/include/hyperion/BGEffectHandler.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include + +/// +/// @brief Handle the background Effect settings, reacts on runtime to settings changes +/// +class BGEffectHandler : public QObject +{ + Q_OBJECT + +public: + BGEffectHandler(Hyperion* hyperion) + : QObject(hyperion) + , _hyperion(hyperion) + { + // listen for config changes + connect(_hyperion, &Hyperion::settingsChanged, this, &BGEffectHandler::handleSettingsUpdate); + + // init + handleSettingsUpdate(settings::BGEFFECT, _hyperion->getSetting(settings::BGEFFECT)); + }; + +private slots: + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) + { + if(type == settings::BGEFFECT) + { + const QJsonObject& BGEffectConfig = config.object(); + + #define BGCONFIG_ARRAY bgColorConfig.toArray() + // clear bg prioritiy + _hyperion->clear(254); + // initial background effect/color + if (BGEffectConfig["enable"].toBool(true)) + { + const QString bgTypeConfig = BGEffectConfig["type"].toString("effect"); + const QString bgEffectConfig = BGEffectConfig["effect"].toString("Warm mood blobs"); + const QJsonValue bgColorConfig = BGEffectConfig["color"]; + if (bgTypeConfig.contains("color")) + { + ColorRgb bg_color = { + (uint8_t)BGCONFIG_ARRAY.at(0).toInt(0), + (uint8_t)BGCONFIG_ARRAY.at(1).toInt(0), + (uint8_t)BGCONFIG_ARRAY.at(2).toInt(0) + }; + _hyperion->setColor(254, bg_color); + Info(Logger::getInstance("HYPERION"),"Inital background color set (%d %d %d)",bg_color.red,bg_color.green,bg_color.blue); + } + else + { + int result = _hyperion->setEffect(bgEffectConfig, 254); + Info(Logger::getInstance("HYPERION"),"Inital background effect '%s' %s", QSTRING_CSTR(bgEffectConfig), ((result == 0) ? "started" : "failed")); + } + } + + #undef BGCONFIG_ARRAY + } + }; + +private: + /// Hyperion instance pointer + Hyperion* _hyperion; +}; diff --git a/include/hyperion/CaptureCont.h b/include/hyperion/CaptureCont.h new file mode 100644 index 00000000..a5b5059e --- /dev/null +++ b/include/hyperion/CaptureCont.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include + +class Hyperion; +class QTimer; + +/// +/// @brief Capture Control class which is a interface to the HyperionDaemon native capture classes. +/// It controls the instance based enable/disable of capture feeds and PriorityMuxer registrations +/// +class CaptureCont : public QObject +{ + Q_OBJECT +public: + CaptureCont(Hyperion* hyperion); + ~CaptureCont(); + + void setSystemCaptureEnable(const bool& enable); + void setV4LCaptureEnable(const bool& enable); + +private slots: + /// + /// @brief Handle component state change of V4L and SystemCapture + /// @param component The component from enum + /// @param enable The new state + /// + void componentStateChanged(const hyperion::Components component, bool enable); + + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + + /// + /// @brief forward system image + /// @param image The image + /// + void handleSystemImage(const Image& image); + + /// + /// @brief forward v4l image + /// @param image The image + /// + void handleV4lImage(const Image & image); + + /// + /// @brief Is called from _v4lInactiveTimer to set source after specific time to inactive + /// + void setV4lInactive(); + + /// + /// @brief Is called from _systemInactiveTimer to set source after specific time to inactive + /// + void setSystemInactive(); + +private: + /// Hyperion instance + Hyperion* _hyperion; + + /// Reflect state of System capture and prio + bool _systemCaptEnabled; + quint8 _systemCaptPrio; + QTimer* _systemInactiveTimer; + + /// Reflect state of v4l capture and prio + bool _v4lCaptEnabled; + quint8 _v4lCaptPrio; + QTimer* _v4lInactiveTimer; +}; diff --git a/include/hyperion/ComponentRegister.h b/include/hyperion/ComponentRegister.h index 5ae442b0..4f1a13c6 100644 --- a/include/hyperion/ComponentRegister.h +++ b/include/hyperion/ComponentRegister.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include // STL includes @@ -8,21 +8,61 @@ #include +class Hyperion; + +/// +/// @brief The component register reflects and manages the current state of all components and Hyperion as a whole +/// It emits also real component state changes (triggert from the specific component), which can be used for listening APIs (Network Clients/Plugins) +/// class ComponentRegister : public QObject { Q_OBJECT public: - ComponentRegister(); + ComponentRegister(Hyperion* hyperion); ~ComponentRegister(); + /// + /// @brief Enable or disable Hyperion (all components) + /// @param state The new state of Hyperion + /// + /// @return Returns true on success, false when Hyperion is already at the requested state + /// + bool setHyperionEnable(const bool& state); + + /// + /// @brief Check if a component is currently enabled + /// @param comp The component from enum + /// @return True if component is running else false. Not found is -1 + /// + int isComponentEnabled(const hyperion::Components& comp) const; + + /// contains all components and their state std::map getRegister() { return _componentStates; }; +signals: + /// + /// @brief Emits whenever a component changed (really) the state + /// @param comp The component + /// @param state The new state of the component + /// + void updatedComponentState(const hyperion::Components comp, const bool state); + public slots: + /// + /// @brief is called whenever a component change a state, DO NOT CALL FROM API (use hyperion->setComponentState() instead) + /// @param comp The component + /// @param state The new state of the component + /// void componentStateChanged(const hyperion::Components comp, const bool activated); private: - std::map _componentStates; + /// Hyperion instance + Hyperion * _hyperion; + /// Logger instance Logger * _log; + /// current state of all components + std::map _componentStates; + /// on hyperion off we save the previous states of all components + std::map _prevComponentStates; }; - diff --git a/include/hyperion/Grabber.h b/include/hyperion/Grabber.h index fa81a57b..b35af885 100644 --- a/include/hyperion/Grabber.h +++ b/include/hyperion/Grabber.h @@ -6,10 +6,14 @@ #include #include #include +#include #include #include - +/// +/// @brief The Grabber class is responsible to apply image resizes (with or without ImageResampler) +/// Overwrite the videoMode with setVideoMode() +/// Overwrite setCropping() class Grabber : public QObject { Q_OBJECT @@ -24,14 +28,72 @@ public: /// virtual void setVideoMode(VideoMode mode); + /// + /// @brief Apply new crop values, on errors reject the values + /// virtual void setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom); - /// gets resulting height of image + /// + /// @brief Apply new width/height values, on errors (collide with cropping) reject the values + /// @return True on success else false + /// + virtual bool setWidthHeight(int width, int height); + + /// + /// @brief Apply new pixelDecimation (used from x11 and qt) + /// + virtual void setPixelDecimation(int pixelDecimation) {}; + + /// + /// @brief Apply new signalThreshold (used from v4l) + /// + virtual void setSignalThreshold( + double redSignalThreshold, + double greenSignalThreshold, + double blueSignalThreshold, + int noSignalCounterThreshold = 50) {}; + /// + /// @brief Apply new SignalDetectionOffset (used from v4l) + /// + virtual void setSignalDetectionOffset( + double verticalMin, + double horizontalMin, + double verticalMax, + double horizontalMax) {}; + + /// + /// @brief Apply SignalDetectionEnable (used from v4l) + /// + virtual void setSignalDetectionEnable(bool enable) {}; + + /// + /// @brief Apply device and videoStanded (used from v4l) + /// + virtual void setDeviceVideoStandard(QString device, VideoStandard videoStandard) {}; + + /// + /// @brief Apply display index (used from qt) + /// + virtual void setDisplayIndex(int index) {}; + + /// + /// @brief Apply path for device (used from framebuffer) + /// + virtual void setDevicePath(const QString& path) {}; + + /// + /// @brief get current resulting height of image (after crop) + /// virtual const int getImageWidth() { return _width; }; - /// gets resulting width of image + /// + /// @brief get current resulting width of image (after crop) + /// virtual const int getImageHeight() { return _height; }; + /// + /// @brief Prevent the real capture implementation from capturing if disabled + /// void setEnabled(bool enable); protected: diff --git a/include/hyperion/GrabberWrapper.h b/include/hyperion/GrabberWrapper.h index 7e893a82..57c92808 100644 --- a/include/hyperion/GrabberWrapper.h +++ b/include/hyperion/GrabberWrapper.h @@ -1,27 +1,30 @@ #pragma once #include -#include +#include +#include #include #include #include #include -#include -#include #include #include #include +#include -class ImageProcessor; class Grabber; -class DispmanxFrameGrabber; +class GlobalSignals; +class QTimer; +/// +/// This class will be inherted by FramebufferWrapper and others which contains the real capture interface +/// class GrabberWrapper : public QObject { Q_OBJECT public: - GrabberWrapper(QString grabberName, Grabber * ggrabber, unsigned width, unsigned height, const unsigned updateRate_Hz, const int priority, hyperion::Components grabberComponentId=hyperion::COMP_GRABBER); + GrabberWrapper(QString grabberName, Grabber * ggrabber, unsigned width, unsigned height, const unsigned updateRate_Hz); virtual ~GrabberWrapper(); @@ -35,8 +38,6 @@ public: /// virtual void stop(); - void setImageProcessorEnabled(bool enable); - static QStringList availableGrabbers(); public: @@ -45,84 +46,69 @@ public: { unsigned w = grabber.getImageWidth(); unsigned h = grabber.getImageHeight(); - if (_imageProcessorEnabled && ( _image.width() != w || _image.height() != h)) + if ( _image.width() != w || _image.height() != h) { - _processor->setSize(w, h); _image.resize(w, h); } int ret = grabber.grabFrame(_image); if (ret >= 0) { - emit emitImage(_priority, _image, _timeout_ms); - _processor->process(_image, _ledColors); - setColors(_ledColors, _timeout_ms); + emit systemImage(_image); return true; } return false; } - public slots: - void componentStateChanged(const hyperion::Components component, bool enable); - /// /// virtual method, should perform single frame grab and computes the led-colors /// virtual void action() = 0; - void actionWrapper(); - /// /// Set the video mode (2D/3D) /// @param[in] mode The new video mode /// - virtual void setVideoMode(const VideoMode videoMode); + virtual void setVideoMode(const VideoMode& videoMode); + /// + /// Set the crop values + /// @param cropLeft Left pixel crop + /// @param cropRight Right pixel crop + /// @param cropTop Top pixel crop + /// @param cropBottom Bottom pixel crop + /// virtual void setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom); + /// + /// @brief Handle settings update from HyperionDaemon Settingsmanager emit + /// @param type settingyType from enum + /// @param config configuration object + /// + virtual void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + signals: - void emitImage(int priority, const Image & image, const int timeout_ms); + /// + /// @brief Emit the final processed image + /// + void systemImage(const Image& image); protected: - void setColors(const std::vector &ledColors, const int timeout_ms); - QString _grabberName; - /// Pointer to Hyperion for writing led values - Hyperion * _hyperion; - - /// The priority of the led colors - const int _priority; - /// The timer for generating events with the specified update rate - QTimer _timer; + QTimer* _timer; - /// The update rate [Hz] - const int _updateInterval_ms; - - /// The timeout of the led colors [ms] - const int _timeout_ms; + /// The calced update rate [ms] + int _updateInterval_ms; /// The Logger instance Logger * _log; - // forwarding enabled - bool _forward; - - /// The processor for transforming images to led colors - ImageProcessor * _processor; - - hyperion::Components _grabberComponentId; - Grabber *_ggrabber; /// The image used for grabbing frames Image _image; - - /// The list with computed led colors - std::vector _ledColors; - - bool _imageProcessorEnabled; }; diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index e17d853c..7440020b 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -5,15 +5,13 @@ #include // QT includes -#include +//#include #include #include -#include #include #include #include #include -#include #include // hyperion-utils includes @@ -27,7 +25,6 @@ #include #include #include -#include #include // Effect engine includes @@ -35,17 +32,23 @@ #include #include -// bonjour includes -#include -#include +// settings utils +#include // Forward class declaration -class LedDevice; +class QTimer; +class HyperionDaemon; +class ImageProcessor; +class MessageForwarder; class LinearColorSmoothing; -class RgbTransform; class EffectEngine; -class RgbChannelAdjustment; class MultiColorAdjustment; +class ColorAdjustment; +class SettingsManager; +class BGEffectHandler; +class CaptureCont; +class BoblightServer; +class LedDeviceWrapper; /// /// The main class of Hyperion. This gives other 'users' access to the attached LedDevice through @@ -57,8 +60,6 @@ class Hyperion : public QObject public: /// Type definition of the info structure used by the priority muxer typedef PriorityMuxer::InputInfo InputInfo; - typedef QMap PriorityRegister; - typedef QMap BonjourRegister; /// /// RGB-Color channel enumeration /// @@ -79,23 +80,56 @@ public: /// /// @brief creates a new Hyperion instance, usually called from the Hyperion Daemon - /// @param[in] qjsonConfig The configuration file + /// @param[in] daemon The Hyperion daemon parent + /// @param[in] instance The instance id /// @param[in] rootPath Root path of all hyperion userdata /// @return Hyperion instance pointer /// - static Hyperion* initInstance(const QJsonObject& qjsonConfig, const QString configFile, const QString rootPath); + static Hyperion* initInstance(HyperionDaemon* daemon, const quint8& instance, const QString configFile, const QString rootPath); /// /// @brief Get a pointer of this Hyperion instance - /// @return Hyperion instance pointer + /// @return Hyperion instance pointer /// static Hyperion* getInstance(); + /// + /// @brief Get a pointer to the effect engine + /// @return EffectEngine instance pointer + /// + EffectEngine* getEffectEngineInstance() { return _effectEngine; }; + + /// + /// @brief Get a pointer to the priorityMuxer instance + /// @return PriorityMuxer instance pointer + /// + PriorityMuxer* getMuxerInstance() { return &_muxer; }; + + ImageProcessor* getImageProcessor() { return _imageProcessor; }; + + /// + /// @brief Get a setting by settings::type from SettingsManager + /// @param type The settingsType from enum + /// @return Data Document + /// + QJsonDocument getSetting(const settings::type& type); + + /// + /// @brief Save a complete json config + /// @param config The entire config object + /// @param correct If true will correct json against schema before save + /// @return True on success else false + /// + bool saveSettings(QJsonObject config, const bool& correct = false); + /// /// Returns the number of attached leds /// unsigned getLedCount() const; + /// + /// @brief Return the size of led grid + /// QSize getLedGridSize() const { return _ledGridSize; }; /// @@ -113,7 +147,7 @@ public: bool isCurrentPriority(const int priority) const; /// - /// Returns a list of active priorities + /// Returns a list of all registered priorities /// /// @return The list with priorities /// @@ -124,14 +158,25 @@ public: /// /// @param[in] priority The priority channel /// - /// @return The information of the given + /// @return The information of the given, a not found priority will return lowest priority as fallback /// - /// @throw std::runtime_error when the priority channel does not exist - /// - const InputInfo& getPriorityInfo(const int priority) const; + const InputInfo getPriorityInfo(const int priority) const; - /// Reload the list of available effects - void reloadEffects(); + /// + /// @brief Save an effect + /// @param obj The effect args + /// @param[out] resultMsg The feedback message + /// @return True on success else false + /// + const bool saveEffect(const QJsonObject& obj, QString& resultMsg); + + /// + /// @brief Delete an effect by name. + /// @param[in] effectName The effect name to delete + /// @param[out] resultMsg The message on error + /// @return True on success else false + /// + const bool deleteEffect(const QString& effectName, QString& resultMsg); /// Get the list of available effects /// @return The list of available effects @@ -145,27 +190,29 @@ public: /// @return The list of available effect schema files const std::list &getEffectSchemas(); - /// gets the current json config object + /// gets the current json config object from SettingsManager /// @return json config - const QJsonObject& getQJsonConfig() { return _qjsonConfig; }; + const QJsonObject& getQJsonConfig(); + + /// get path+filename of configfile + /// @return the current config path+filename + QString getConfigFilePath() { return _configFile; }; /// get filename of configfile /// @return the current config filename - QString getConfigFileName() { return _configFile; }; + QString getConfigFileName() const; - /// register a input source to a priority channel - /// @param name uniq name of input source - /// @param origin External setter - /// @param priority priority channel - void registerPriority(const QString &name, const int priority); + /// + /// @brief Register a new input by priority, the priority is not active (timeout -100 isn't muxer recognized) until you start to update the data with setInput() + /// A repeated call to update the base data of a known priority won't overwrite their current timeout + /// @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] smooth_cfg The smooth id to use + /// + void registerInput(const int priority, const hyperion::Components& component, const QString& origin = "System", const QString& owner = "", unsigned smooth_cfg = 0); - /// unregister a input source to a priority channel - /// @param name uniq name of input source - void unRegisterPriority(const QString &name); - - /// gets current priority register - /// @return the priority register - const PriorityRegister& getPriorityRegister() { return _priorityRegister; } /// enable/disable automatic/priorized source selection /// @param enabled the state @@ -178,12 +225,19 @@ public: /// gets current state of automatic/priorized source selection /// @return the state - bool sourceAutoSelectEnabled() { return _sourceAutoSelectEnabled; }; + bool sourceAutoSelectEnabled(); /// - /// Enable/Disable components during runtime + /// @brief Called from components to update their current state. DO NOT CALL FROM USERS + /// @param[in] component The component from enum + /// @param[in] state The state of the component [true | false] /// - /// @param component The component [SMOOTHING, BLACKBORDER, FORWARDER, UDPLISTENER, BOBLIGHT_SERVER, GRABBER] + void setNewComponentState(const hyperion::Components& component, const bool& state); + + /// + /// @brief Enable/Disable components during runtime, called from external API (requests) + /// + /// @param component The component from enum /// @param state The state of the component [true | false] /// void setComponentState(const hyperion::Components component, const bool state); @@ -195,54 +249,70 @@ public: bool configWriteable() { return _configWrite; }; /// gets the methode how image is maped to leds - int getLedMappingType() { return _ledMAppingType; }; - - /// get the configuration - QJsonObject getConfig() { return _qjsonConfig; }; + const int & getLedMappingType(); /// get the root path for all hyperion user data files - QString getRootPath() { return _rootPath; }; + const QString &getRootPath() { return _rootPath; }; - /// unique id per instance - QString id; + /// get unique id per instance + const QString &getId(){ return _id; }; + /// set unique id + void setId(QString id){ _id = id; }; int getLatchTime() const; /// forward smoothing config unsigned addSmoothingConfig(int settlingTime_ms, double ledUpdateFrequency_hz=25.0, unsigned updateDelay=0); - VideoMode getCurrentVideoMode() { return _videoMode; }; + const VideoMode & getCurrentVideoMode(); + + /// + /// @brief Get the current active led device + /// @return The device nam + /// e + const QString & getActiveDevice(); public slots: + /// + /// @brief Update the current color of a priority (prev registered with registerInput()) + /// DO NOT use this together with setInputImage() at the same time! + /// @param priority The priority to update + /// @param ledColors The colors + /// @param timeout_ms The new timeout (defaults to -1 endless) + /// @param clearEffect Should be true when NOT called from an effect + /// @return True on success, false when priority is not found + /// + const bool setInput(const int priority, const std::vector& ledColors, const int timeout_ms = -1, const bool& clearEffect = true); + + /// + /// @brief Update the current image of a priority (prev registered with registerInput()) + /// DO NOT use this together with setInput() at the same time! + /// @param priority The priority to update + /// @param image The new image + /// @param timeout_ms The new timeout (defaults to -1 endless) + /// @param clearEffect Should be true when NOT called from an effect + /// @return True on success, false when priority is not found + /// + const bool setInputImage(const int priority, const Image& image, int64_t timeout_ms = -1, const bool& clearEffect = true); + + /// + /// @brief Set the given priority to inactive + /// @param priority The priority + /// @return True on success false if not found + /// + const bool setInputInactive(const quint8& priority); + /// /// Writes a single color to all the leds for the given time and priority + /// Registers comp color or provided type against muxer + /// Should be never used to update leds continuous /// /// @param[in] priority The priority of the written color /// @param[in] ledColor The color to write to the leds + /// @param[in] origin The setter /// @param[in] timeout_ms The time the leds are set to the given color [ms] /// - void setColor(int priority, const ColorRgb &ledColor, const int timeout_ms, bool clearEffects = true); - - /// - /// Writes the given colors to all leds for the given time and priority - /// - /// @param[in] priority The priority of the written colors - /// @param[in] ledColors The colors to write to the leds - /// @param[in] timeout_ms The time the leds are set to the given colors [ms] - /// @param[in] component The current component - /// @param[in] origin Who set it - /// @param[in] smoothCfg smoothing config id - /// - void setColors(int priority, const std::vector &ledColors, const int timeout_ms, bool clearEffects = true, hyperion::Components component=hyperion::COMP_INVALID, const QString origin="System", unsigned smoothCfg=SMOOTHING_MODE_DEFAULT); - - /// - /// Writes the given colors to all leds for the given time and priority - /// - /// @param[in] priority The priority of the written colors - /// @param[in] ledColors The colors to write to the leds - /// @param[in] timeout_ms The time the leds are set to the given colors [ms] - /// - void setImage(int priority, const Image & image, int duration_ms); + void setColor(int priority, const ColorRgb &ledColor, const int timeout_ms = -1, const QString& origin = "System" ,bool clearEffects = true); /// /// Returns the list with unique adjustment identifiers @@ -256,12 +326,6 @@ public slots: /// ColorAdjustment * getAdjustment(const QString& id); - /// - /// Returns MessageForwarder Object - /// @return instance of message forwarder object - /// - MessageForwarder * getForwarder(); - /// Tell Hyperion that the corrections have changed and the leds need to be updated void adjustmentsUpdated(); @@ -270,11 +334,12 @@ public slots: /// lower priority channel (or off if no more channels are set) /// /// @param[in] priority The priority channel + /// @return True on success else false (not found) /// - void clear(int priority); + const bool clear(int priority); /// - /// Clears all priority channels. This will switch the leds off until a new priority is written. + /// @brief Clears all priority channels. This will switch the leds off until a new priority is written. /// void clearall(bool forceClearAll=false); @@ -289,47 +354,27 @@ public slots: /// @param args arguments of the effect script /// @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 = -1, const QString & pythonScript = "", const QString & origin="System"); + int setEffect(const QString &effectName + , const QJsonObject &args + , int priority + , int timeout = -1 + , const QString &pythonScript = "" + , const QString &origin="System" + , const QString &imageData = "" + ); - /// sets the methode how image is maped to leds - void setLedMappingType(int mappingType); - - /// - Hyperion::BonjourRegister getHyperionSessions(); - - /// Slot which is called, when state of hyperion has been changed - void hyperionStateChanged(); + /// sets the methode how image is maped to leds at ImageProcessor + void setLedMappingType(const int& mappingType); /// /// Set the video mode (2D/3D) /// @param[in] mode The new video mode /// - void setVideoMode(VideoMode mode); + void setVideoMode(const VideoMode& mode); public: static Hyperion *_hyperion; - static ColorOrder createColorOrder(const QJsonObject & deviceConfig); - /** - * 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 QJsonValue & ledsConfig, const ColorOrder deviceOrder); - static LedString createLedStringClone(const QJsonValue & ledsConfig, const ColorOrder deviceOrder); - - static MultiColorAdjustment * createLedColorsAdjustment(const unsigned ledCnt, const QJsonObject & colorAdjustmentConfig); - static ColorAdjustment * createColorAdjustment(const QJsonObject & adjustmentConfig); - static RgbTransform * createRgbTransform(const QJsonObject& colorConfig); - static RgbChannelAdjustment * createRgbChannelAdjustment(const QJsonObject & colorConfig, const QString channelName, const int defaultR, const int defaultG, const int defaultB); - - static LinearColorSmoothing * createColorSmoothing(const QJsonObject & smoothingConfig, LedDevice* leddevice); - static MessageForwarder * createMessageForwarder(const QJsonObject & forwarderConfig); - static QSize getLedLayoutGridSize(const QJsonValue& ledsConfig); - signals: /// Signal which is emitted when a priority channel is actively cleared /// This signal will not be emitted when a priority channel time out @@ -339,20 +384,71 @@ signals: /// This signal will not be emitted when a priority channel time out void allChannelsCleared(); + /// + /// @brief Emits whenever a user request a component state change, it's up the component to listen + /// and update the component state at the componentRegister + /// @param component The component from enum + /// @param enabled The new state of the component + /// void componentStateChanged(const hyperion::Components component, bool enabled); - void imageToLedsMappingChanged(int mappingType); - void emitImage(int priority, const Image & image, const int timeout_ms); + /// + /// @brief Emits whenever the imageToLedsMapping has changed + /// @param mappingType The new mapping type + /// + void imageToLedsMappingChanged(const int& mappingType); + + /// + /// @brief Emits whenever the visible priority delivers a image which is applied in update() + /// priorities with ledColors won't emit this signal + /// @param image The current image + /// + void currentImage(const Image & image); + void closing(); /// Signal which is emitted, when a new json message should be forwarded void forwardJsonMessage(QJsonObject); - /// Signal which is emitted, after the hyperionStateChanged has been processed with a emit count blocker (250ms interval) - void sendServerInfo(); + /// Signal which is emitted, when a new proto image should be forwarded + void forwardProtoMessage(Image); - /// Signal emitted when a 3D movie is detected - void videoMode(VideoMode mode); + /// + /// @brief Is emitted from clients who request a videoMode change + /// + void videoMode(const VideoMode& mode); + + /// + /// @brief A new videoMode was requested (called from Daemon!) + /// + void newVideoMode(const VideoMode& mode); + + /// + /// @brief Emits whenever a config part changed. SIGNAL PIPE helper for SettingsManager -> HyperionDaemon + /// @param type The settings type from enum + /// @param data The data as QJsonDocument + /// + void settingsChanged(const settings::type& type, const QJsonDocument& data); + + /// + /// @brief Emits whenever the adjustments have been updated + /// + void adjustmentChanged(); + + /// + /// @brief Signal pipe from EffectEngine to external, emits when effect list has been updated + /// + void effectListUpdated(); + + /// + /// @brief Emits whenever new data should be pushed to the LedDeviceWrapper which forwards it to the threaded LedDevice + /// + void ledDeviceData(const std::vector& ledValues); + + /// + /// @brief Emits whenever new untransformed ledColos data is available, reflects the current visible device + /// + void rawLedColors(const std::vector& ledValues); private slots: /// @@ -361,13 +457,23 @@ private slots: /// void update(); - void currentBonjourRecordsChanged(const QList &list); - void bonjourRecordResolved(const QHostInfo &hostInfo, int port); - void bonjourResolve(); - /// check for configWriteable and modified changes, called by _fsWatcher or fallback _cTimer void checkConfigState(QString cfile = NULL); + /// + /// @brief Apply ComponentRegister emits for COMP_ALL. Enables/Disables core timers + /// @param comp The component + /// @param state The new state of the component + /// + void updatedComponentState(const hyperion::Components comp, const bool state); + + /// + /// @brief Apply settings updates for LEDS and COLOR + /// @param type The type from enum + /// @param config The configuration + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + private: /// @@ -375,7 +481,16 @@ private: /// /// @param[in] qjsonConfig The Json configuration /// - Hyperion(const QJsonObject& qjsonConfig, const QString configFile, const QString rootPath); + Hyperion(HyperionDaemon* daemon, const quint8& instance, const QString configFile, const QString rootPath); + + /// The parent Hyperion Daemon + HyperionDaemon* _daemon; + + /// Settings manager of this instance + SettingsManager* _settingsManager; + + /// Register that holds component states + ComponentRegister _componentRegister; /// The specifiation of the led frame construction and picture integration LedString _ledString; @@ -383,6 +498,9 @@ private: /// specifiation of cloned leds LedString _ledStringClone; + /// Image Processor + ImageProcessor* _imageProcessor; + std::vector _ledStringColorOrder; /// The priority muxer @@ -391,8 +509,8 @@ private: /// The adjustment from raw colors to led colors MultiColorAdjustment * _raw2ledAdjustment; - /// The actual LedDevice - LedDevice * _device; + /// The actual LedDeviceWrapper + LedDeviceWrapper* _ledDeviceWrapper; /// The smoothing LedDevice LinearColorSmoothing * _deviceSmooth; @@ -400,24 +518,17 @@ private: /// Effect engine EffectEngine * _effectEngine; - // proto and json Message forwarder + // Message forwarder MessageForwarder * _messageForwarder; - // json configuration - const QJsonObject& _qjsonConfig; - /// the name of config file QString _configFile; /// root path for all hyperion user data files QString _rootPath; - /// The timer for handling priority channel timeouts - QTimer _timer; - QTimer _timerBonjourResolver; - - /// buffer for leds - std::vector _ledBuffer; + /// unique id per instance + QString _id; /// Logger instance Logger * _log; @@ -425,32 +536,16 @@ private: /// count of hardware leds unsigned _hwLedCount; - ComponentRegister _componentRegister; - - /// register of input sources and it's prio channel - PriorityRegister _priorityRegister; - - /// flag indicates state for autoselection of input source - bool _sourceAutoSelectEnabled; - - /// holds the current priority channel that is manualy selected - int _currentSourcePriority; - QByteArray _configHash; QSize _ledGridSize; - int _ledMAppingType; - + /// Store the previous compID for smarter update() hyperion::Components _prevCompId; - BonjourServiceBrowser _bonjourBrowser; - BonjourServiceResolver _bonjourResolver; - BonjourRegister _hyperionSessions; - QString _bonjourCurrentServiceToResolve; /// Observe filesystem changes (_configFile), if failed use Timer QFileSystemWatcher _fsWatcher; - QTimer _cTimer; + QTimer* _cTimer; /// holds the prev states of configWriteable and modified bool _prevConfigMod = false; @@ -460,9 +555,17 @@ private: bool _configMod = false; bool _configWrite = true; - /// timers to handle severinfo blocking - QTimer _fsi_timer; - QTimer _fsi_blockTimer; + /// Background effect instance, kept active to react on setting changes + BGEffectHandler* _BGEffectHandler; + /// Capture control for Daemon native capture + CaptureCont* _captureCont; - VideoMode _videoMode; + // lock Hyperion::update() for exec + bool _lockUpdate = false; + + /// buffer for leds (with adjustment) + std::vector _ledBuffer; + + /// Boblight instance + BoblightServer* _boblightServer; }; diff --git a/include/hyperion/ImageProcessor.h b/include/hyperion/ImageProcessor.h index 935fcaea..d2c16216 100644 --- a/include/hyperion/ImageProcessor.h +++ b/include/hyperion/ImageProcessor.h @@ -6,14 +6,18 @@ #include // Hyperion includes -#include #include #include #include +// settings +#include + // Black border includes #include +class Hyperion; + /// /// The ImageProcessor translates an RGB-image to RGB-values for the leds. The processing is /// performed in two steps. First the average color per led-region is computed. Second a @@ -24,14 +28,16 @@ class ImageProcessor : public QObject Q_OBJECT public: + /// + /// Constructs an image-processor for translating an image to led-color values based on the + /// given led-string specification + /// @param[in] ledString LedString data + /// @param[in] hyperion Hyperion instance pointer + /// + ImageProcessor(const LedString& ledString, Hyperion* hyperion); ~ImageProcessor(); - /// - /// Returns the number of attached leds - /// - unsigned getLedCount() const; - /// /// Specifies the width and height of 'incomming' images. This will resize the buffer-image to /// match the given size. @@ -42,20 +48,39 @@ public: /// void setSize(const unsigned width, const unsigned height); + /// + /// @brief Update the led string (eg on settings change) + /// + void setLedString(const LedString& ledString); + /// Returns starte of black border detector bool blackBorderDetectorEnabled(); - /// Returns starte of black border detector - int ledMappingType(); + /// Returns the current _userMappingType, this may not be the current applied type! + const int & getUserLedMappingType() { return _userMappingType; }; + + /// Returns the current _mappingType + const int & ledMappingType() { return _mappingType; }; static int mappingTypeToInt(QString mappingType); static QString mappingTypeToStr(int mappingType); -public slots: - /// Enable or disable the black border detector - void enableBlackBorderDetector(bool enable); + /// + /// @brief Set the Hyperion::update() requestes led mapping type. This type is used in favour of type set with setLedMappingType. + /// If you don't want to force a mapType set this to -1 (user choice will be set) + /// @param mapType The new mapping type + /// + void setHardLedMappingType(int mapType); - /// Enable or disable the black border detector +public slots: + /// Enable or disable the black border detector based on component + void setBlackbarDetectDisable(bool enable); + + /// + /// @brief Set the user requested led mapping. + /// The type set with setHardLedMappingType() will be used in favour to respect comp specific settings + /// @param mapType The new mapping type + /// void setLedMappingType(int mapType); public: @@ -101,7 +126,7 @@ public: } else { - Warning(_log, "ImageProcessor::process called without image size 0"); + Warning(_log, "ImageProcessor::process called with image size 0"); } // return the computed colors @@ -134,7 +159,7 @@ public: } else { - Warning(_log, "ImageProcessor::process called without image size 0"); + Warning(_log, "Called with image size 0"); } } @@ -150,18 +175,6 @@ public: bool getScanParameters(size_t led, double & hscanBegin, double & hscanEnd, double & vscanBegin, double & vscanEnd) const; private: - /// Friend declaration of the factory for creating ImageProcessor's - friend class ImageProcessorFactory; - - /// - /// Constructs an image-processor for translating an image to led-color values based on the - /// given led-string specification - /// - /// @param[in] ledString The led-string specification - /// @param[in] blackborderThreshold The threshold which the blackborder detector should use - /// - ImageProcessor(const LedString &ledString, const QJsonObject &blackborderConfig); - /// /// Performs black-border detection (if enabled) on the given image /// @@ -172,7 +185,7 @@ private: { if (!_borderProcessor->enabled() && ( _imageToLeds->horizontalBorder()!=0 || _imageToLeds->verticalBorder()!=0 )) { - Debug(Logger::getInstance("BLACKBORDER"), "disabled, reset border"); + Debug(_log, "Reset border"); _borderProcessor->process(image); delete _imageToLeds; _imageToLeds = new hyperion::ImageToLedsMap(image.width(), image.height(), 0, 0, _ledString.leds()); @@ -180,8 +193,6 @@ private: if(_borderProcessor->enabled() && _borderProcessor->process(image)) { - //Debug(Logger::getInstance("BLACKBORDER"), "BORDER SWITCH REQUIRED!!"); - const hyperion::BlackBorder border = _borderProcessor->getCurrentBorder(); // Clean up the old mapping @@ -203,10 +214,13 @@ private: } } +private slots: + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + private: Logger * _log; /// The Led-string specification - const LedString _ledString; + LedString _ledString; /// The processor for black border detection hyperion::BlackBorderProcessor * _borderProcessor; @@ -216,4 +230,11 @@ private: /// Type of image 2 led mapping int _mappingType; + /// Type of last requested user type + int _userMappingType; + /// Type of last requested hard type + int _hardMappingType; + + /// Hyperion instance pointer + Hyperion* _hyperion; }; diff --git a/include/hyperion/ImageProcessorFactory.h b/include/hyperion/ImageProcessorFactory.h deleted file mode 100644 index e7381ad0..00000000 --- a/include/hyperion/ImageProcessorFactory.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -// STL includes -#include - -// QT includes -#include - -#include - -// Forward class declaration -class ImageProcessor; - -/// -/// The ImageProcessor is a singleton factor for creating ImageProcessors that translate images to -/// led color values. -/// -class ImageProcessorFactory -{ -public: - /// - /// Returns the 'singleton' instance (creates the singleton if it does not exist) - /// - /// @return The singleton instance of the ImageProcessorFactory - /// - static ImageProcessorFactory& getInstance(); - -public: - /// - /// Initialises this factory with the given led-configuration - /// - /// @param[in] ledString The led configuration - /// @param[in] blackborderConfig Contains the blackborder configuration - /// - void init(const LedString& ledString, const QJsonObject &blackborderConfig, int mappingType); - - /// - /// Creates a new ImageProcessor. The onwership of the processor is transferred to the caller. - /// - /// @return The newly created ImageProcessor - /// - ImageProcessor* newImageProcessor() const; - -private: - /// The Led-string specification - LedString _ledString; - - /// Reference to the blackborder json configuration values - QJsonObject _blackborderConfig; - - // image 2 led mapping type - int _mappingType; -}; diff --git a/include/hyperion/ImageToLedsMap.h b/include/hyperion/ImageToLedsMap.h index 116d4760..637bb050 100644 --- a/include/hyperion/ImageToLedsMap.h +++ b/include/hyperion/ImageToLedsMap.h @@ -7,6 +7,7 @@ // hyperion-utils includes #include +#include // hyperion includes #include @@ -58,7 +59,7 @@ namespace hyperion const unsigned horizontalBorder() const { return _horizontalBorder; }; const unsigned verticalBorder() const { return _verticalBorder; }; - + /// /// Determines the mean-color for each led using the mapping the image given /// at construction. @@ -86,7 +87,12 @@ namespace hyperion void getMeanLedColor(const Image & image, std::vector & ledColors) const { // Sanity check for the number of leds - assert(_colorsMap.size() == ledColors.size()); + //assert(_colorsMap.size() == ledColors.size()); + if(_colorsMap.size() != ledColors.size()) + { + Debug(Logger::getInstance("HYPERION"), "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + return; + } // Iterate each led and compute the mean auto led = ledColors.begin(); @@ -96,7 +102,7 @@ namespace hyperion *led = color; } } - + /// /// Determines the mean-color for each led using the mapping the image given /// at construction. @@ -124,7 +130,13 @@ namespace hyperion void getUniLedColor(const Image & image, std::vector & ledColors) const { // Sanity check for the number of leds - assert(_colorsMap.size() == ledColors.size()); + // assert(_colorsMap.size() == ledColors.size()); + if(_colorsMap.size() != ledColors.size()) + { + Debug(Logger::getInstance("HYPERION"), "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + return; + } + // calculate uni color const ColorRgb color = calcMeanColor(image); @@ -136,11 +148,11 @@ namespace hyperion const unsigned _width; /// The height of the indexed image const unsigned _height; - + const unsigned _horizontalBorder; - + const unsigned _verticalBorder; - + /// The absolute indices into the image for each led std::vector> _colorsMap; @@ -156,7 +168,9 @@ namespace hyperion template ColorRgb calcMeanColor(const Image & image, const std::vector & colors) const { - if (colors.size() == 0) + const auto colorVecSize = colors.size(); + + if (colorVecSize == 0) { return ColorRgb::BLACK; } @@ -165,18 +179,20 @@ namespace hyperion uint_fast16_t cummRed = 0; uint_fast16_t cummGreen = 0; uint_fast16_t cummBlue = 0; + const auto& imgData = image.memptr(); + for (const unsigned colorOffset : colors) { - const Pixel_T& pixel = image.memptr()[colorOffset]; + const auto& pixel = imgData[colorOffset]; cummRed += pixel.red; cummGreen += pixel.green; cummBlue += pixel.blue; } // Compute the average of each color channel - const uint8_t avgRed = uint8_t(cummRed/colors.size()); - const uint8_t avgGreen = uint8_t(cummGreen/colors.size()); - const uint8_t avgBlue = uint8_t(cummBlue/colors.size()); + const uint8_t avgRed = uint8_t(cummRed/colorVecSize); + const uint8_t avgGreen = uint8_t(cummGreen/colorVecSize); + const uint8_t avgBlue = uint8_t(cummBlue/colorVecSize); // Return the computed color return {avgRed, avgGreen, avgBlue}; @@ -199,9 +215,11 @@ namespace hyperion uint_fast16_t cummBlue = 0; const unsigned imageSize = image.width() * image.height(); + const auto& imgData = image.memptr(); + for (unsigned idx=0; idx #include #include +#include +#include +#include // Utils includes #include -class MessageForwarder +#include +#include +#include +#include + +// Hyperion includes +#include + +// Forward declaration +class Hyperion; +class QTcpSocket; +class FlatBufferConnection; + +class MessageForwarder : public QObject { + Q_OBJECT public: - - struct JsonSlaveAddress { - QHostAddress addr; - quint16 port; - }; - - MessageForwarder(); + MessageForwarder(Hyperion* hyperion); ~MessageForwarder(); - + void addJsonSlave(QString slave); void addProtoSlave(QString slave); - bool protoForwardingEnabled(); - bool jsonForwardingEnabled(); - bool forwardingEnabled() { return jsonForwardingEnabled() || protoForwardingEnabled(); }; - QStringList getProtoSlaves(); - QList getJsonSlaves(); +private slots: + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type &type, const QJsonDocument &config); + + /// + /// @brief Handle component state change MessageForwarder + /// @param component The component from enum + /// @param enable The new state + /// + void componentStateChanged(const hyperion::Components component, bool enable); + + /// + /// @brief Handle priority updates from Priority Muxer + /// @param priority The new visible priority + /// + void handlePriorityChanges(const quint8 &priority); + + /// + /// @brief Forward message to all json slaves + /// @param message The JSON message to send + /// + void forwardJsonMessage(const QJsonObject &message); + + /// + /// @brief Forward image to all proto slaves + /// @param image The PROTO image to send + /// + void forwardProtoMessage(const Image &image); + + /// + /// @brief Forward message to a single json slave + /// @param message The JSON message to send + /// @param socket The TCP-Socket with the connection to the slave + /// + void sendJsonMessage(const QJsonObject &message, QTcpSocket *socket); private: - QStringList _protoSlaves; - QList _jsonSlaves; + /// Hyperion instance + Hyperion *_hyperion; + + /// Logger instance + Logger *_log; + + /// Muxer instance + PriorityMuxer *_muxer; + + // JSON connection for forwarding + QStringList _jsonSlaves; + + /// Proto connection for forwarding + QStringList _protoSlaves; + QList _forwardClients; + + /// Flag if forwarder is enabled + bool _forwarder_enabled = true; + + const int _priority; }; diff --git a/libsrc/hyperion/MultiColorAdjustment.h b/include/hyperion/MultiColorAdjustment.h similarity index 98% rename from libsrc/hyperion/MultiColorAdjustment.h rename to include/hyperion/MultiColorAdjustment.h index f6568049..df101122 100644 --- a/libsrc/hyperion/MultiColorAdjustment.h +++ b/include/hyperion/MultiColorAdjustment.h @@ -26,7 +26,7 @@ public: */ void addAdjustment(ColorAdjustment * adjustment); - void setAdjustmentForLed(const QString& id, const unsigned startLed, const unsigned endLed); + void setAdjustmentForLed(const QString& id, const unsigned startLed, unsigned endLed); bool verifyAdjustments() const; diff --git a/include/hyperion/PriorityMuxer.h b/include/hyperion/PriorityMuxer.h index a3316940..2c9fe96d 100644 --- a/include/hyperion/PriorityMuxer.h +++ b/include/hyperion/PriorityMuxer.h @@ -7,23 +7,25 @@ // QT includes #include #include -#include #include #include // Utils includes #include +#include #include // global defines #define SMOOTHING_MODE_DEFAULT 0 #define SMOOTHING_MODE_PAUSE 1 +class QTimer; +class Logger; /// -/// The PriorityMuxer handles the priority channels. Led values input is written to the priority map +/// The PriorityMuxer handles the priority channels. Led values input/ images are written to the priority map /// and the muxer keeps track of all active priorities. The current priority can be queried and per -/// priority the led colors. +/// priority the led colors. Handles also manual/auto selection mode, provides a lot of signals to hook into priority related events /// class PriorityMuxer : public QObject { @@ -36,17 +38,20 @@ public: { /// The priority of this channel int priority; - /// The absolute timeout of the channel int64_t timeoutTime_ms; /// The colors for each led of the channel std::vector ledColors; + /// The raw Image (size should be preprocessed!) + Image image; /// The component hyperion::Components componentId; /// Who set it QString origin; - /// id fo smoothing config + /// id of smoothing config unsigned smooth_cfg; + /// specific owner description + QString owner; }; /// The lowest possible priority, which is used when no priority channels are active @@ -65,12 +70,44 @@ public: /// ~PriorityMuxer(); + /// + /// @brief Start/Stop the PriorityMuxer update timer; On disabled no priority and timeout updates will be performend + /// @param enable The new state + /// + void setEnable(const bool& enable); + + /// @brief Enable or disable auto source selection + /// @param enable True if it should be enabled else false + /// @param update True to update _currentPriority - INTERNAL usage. + /// @return True if changed has been applied, false if the state is unchanged + /// + bool setSourceAutoSelectEnabled(const bool& enabel, const bool& update = true); + + /// + /// @brief Get the state of source auto selection + /// @return True if enabled, else false + /// + bool isSourceAutoSelectEnabled() const { return _sourceAutoSelectEnabled; }; + + /// + /// @brief Overwrite current lowest piority with manual selection; On success disables aito selection + /// @param priority The + /// @return True on success, false if priority not found + /// + bool setPriority(const uint8_t priority); + + /// + /// @brief Update all ledColos with min length of >= 1 to fit the new led length + /// @param[in] ledCount The count of leds + /// + void updateLedColorsLength(const int& ledCount); + /// /// Returns the current priority /// /// @return The current priority /// - int getCurrentPriority() const; + int getCurrentPriority() const { return _currentPriority; } /// /// Returns the state (enabled/disabled) of a specific priority channel @@ -87,69 +124,141 @@ public: QList getPriorities() const; /// - /// Returns the information of a specified priority channel + /// Returns the information of a specified priority channel. + /// If a priority is no longer available the _lowestPriorityInfo (255) is returned /// /// @param priority The priority channel /// /// @return The information for the specified priority channel /// - /// @throws std::runtime_error if the priority channel does not exist - /// - const InputInfo& getInputInfo(const int priority) const; + const InputInfo getInputInfo(const int priority) const; /// - /// Sets/Updates the data for a priority channel + /// @brief Register a new input by priority, the priority is not active (timeout -100 isn't muxer recognized) until you start to update the data with setInput() + /// A repeated call to update the base data of a known priority won't overwrite their current timeout + /// @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 Speicifc owner string, might be empty + /// @param[in] smooth_cfg The smooth id to use /// - /// @param[in] priority The priority of the channel - /// @param[in] ledColors The led colors of the priority channel - /// @param[in] timeoutTime_ms The absolute timeout time of the channel - /// @param[in] component The component of the channel - /// @param[in] origin Who set the channel - /// - void setInput(const int priority, const std::vector& ledColors, const int64_t timeoutTime_ms=-1, hyperion::Components component=hyperion::COMP_INVALID, const QString origin="System", unsigned smooth_cfg=SMOOTHING_MODE_DEFAULT); + void registerInput(const int priority, const hyperion::Components& component, const QString& origin = "System", const QString& owner = "", unsigned smooth_cfg = SMOOTHING_MODE_DEFAULT); /// - /// Clears the specified priority channel + /// @brief Update the current color of a priority (prev registered with registerInput()) + /// @param priority The priority to update + /// @param ledColors The colors + /// @param timeout_ms The new timeout (defaults to -1 endless) + /// @return True on success, false when priority is not found + /// + const bool setInput(const int priority, const std::vector& ledColors, int64_t timeout_ms = -1); + + /// + /// @brief Update the current image of a priority (prev registered with registerInput()) + /// @param priority The priority to update + /// @param image The new image + /// @param timeout_ms The new timeout (defaults to -1 endless) + /// @return True on success, false when priority is not found + /// + const bool setInputImage(const int priority, const Image& image, int64_t timeout_ms = -1); + + /// + /// @brief Set the given priority to inactive + /// @param priority The priority + /// @return True on success false if not found + /// + const bool setInputInactive(const quint8& priority); + + /// + /// Clears the specified priority channel and update _currentPriority on success /// /// @param[in] priority The priority of the channel to clear + /// @return True if priority has been cleared else false (not found) /// - void clearInput(const int priority); + const bool clearInput(const uint8_t priority); /// /// Clears all priority channels /// void clearAll(bool forceClearAll=false); +signals: + /// + /// @brief Signal which emits when a effect or color with timeout > -1 is running, once per second + /// + void timeRunner(); + + /// + /// @brief A priority has been added (registerInput()) or deleted, method clear or timeout clear + /// @param priority The priority which has changed + /// @param state If true it was added else it was removed! + /// + void priorityChanged(const quint8& priority, const bool& state); + + /// + /// @brief Emits whenever the visible priority has changed + /// @param priority The new visible priority + /// + void visiblePriorityChanged(const quint8& priority); + + /// + /// @brief Emits whenever a priority changes active state + /// @param priority The priority who changed the active state + /// @param state The new state, state true = active else false + /// + void activeStateChanged(const quint8& priority, const bool& state); + + /// + /// @brief Emits whenever the auto selection state has been changed + /// @param state The new state of auto selection; True enabled else false + /// + void autoSelectChanged(const bool& state); + + /// + /// @brief Emits whenever something changes which influences the priorities listing + /// Emits also in 1s interval when a COLOR or EFFECT is running with a timeout > -1 + /// + void prioritiesChanged(void); + + /// + /// internal used signal to resolve treading issues with timer + /// + void signalTimeTrigger(); + +private slots: + /// + /// Slot which is called to adapt to 1s interval for signal timeRunner() / prioritiesChanged() + /// + void timeTrigger(); + /// /// Updates the current time. Channels with a configured time out will be checked and cleared if /// required. /// - /// @param[in] now The current time - /// - void setCurrentTime(const int64_t& now); - -signals: - /// - /// Signal which is called, when a effect or color with timeout is running, once per second - /// - void timerunner(); - -private slots: - /// - /// Slots which is called to adapt to 1s interval for signal timerunner() - /// - void emitReq(); + void setCurrentTime(void); private: + /// Logger instance + Logger* _log; + /// The current priority (lowest value in _activeInputs) int _currentPriority; + /// The manual select priority set with setPriority + int _manualSelectedPriority; + /// The mapping from priority channel to led-information QMap _activeInputs; /// The information of the lowest priority channel InputInfo _lowestPriorityInfo; - QTimer _timer; - QTimer _blockTimer; + // Reflect the state of auto select + bool _sourceAutoSelectEnabled; + + // Timer to update Muxer times independent + QTimer* _updateTimer; + + QTimer* _timer; + QTimer* _blockTimer; }; diff --git a/include/hyperion/SettingsManager.h b/include/hyperion/SettingsManager.h new file mode 100644 index 00000000..33a43777 --- /dev/null +++ b/include/hyperion/SettingsManager.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +// qt incl +#include + +class Hyperion; + +/// +/// @brief Manage the settings read write from/to config file, on settings changed will emit a signal to update components accordingly +/// +class SettingsManager : public QObject +{ + Q_OBJECT +public: + /// + /// @brief Construct a settings manager and assign a hyperion instance + /// @params hyperion The parent hyperion instance + /// @params instance Instance number of Hyperion + /// + SettingsManager(Hyperion* hyperion, const quint8& instance, const QString& configFile); + + /// + /// @brief Construct a settings manager for HyperionDaemon + /// + SettingsManager(const quint8& instance, const QString& configFile); + ~SettingsManager(); + + /// + /// @brief Save a complete json config + /// @param config The entire config object + /// @param correct If true will correct json against schema before save + /// @return True on success else false + /// + const bool saveSettings(QJsonObject config, const bool& correct = false); + + /// + /// @brief get a single setting json from config + /// @param type The settings::type from enum + /// @return The requested json data as QJsonDocument + /// + const QJsonDocument getSetting(const settings::type& type); + + /// + /// @brief get the full settings object of this instance (with global settings) + /// @return The requested json + /// + const QJsonObject & getSettings() { return _qconfig; }; + +signals: + /// + /// @brief Emits whenever a config part changed. + /// @param type The settings type from enum + /// @param data The data as QJsonDocument + /// + void settingsChanged(const settings::type& type, const QJsonDocument& data); + +private: + /// Hyperion instance + Hyperion* _hyperion; + + /// Logger instance + Logger* _log; + + /// the schema + static QJsonObject schemaJson; + + /// the current config of this instance + QJsonObject _qconfig; +}; diff --git a/include/jsonserver/JsonServer.h b/include/jsonserver/JsonServer.h index 912e14ac..19b1e374 100644 --- a/include/jsonserver/JsonServer.h +++ b/include/jsonserver/JsonServer.h @@ -1,14 +1,18 @@ #pragma once // Qt includes -#include #include // Hyperion includes -#include +#include #include +#include +class QTcpServer; +class QTcpSocket; class JsonClientConnection; +class BonjourServiceRegister; +class NetOrigin; /// /// This class creates a TCP server which accepts connections wich can then send @@ -22,10 +26,9 @@ class JsonServer : public QObject public: /// /// JsonServer constructor - /// @param hyperion Hyperion instance - /// @param port port number on which to start listening for connections + /// @param The configuration /// - JsonServer(uint16_t port = 19444); + JsonServer(const QJsonDocument& config); ~JsonServer(); /// @@ -45,26 +48,17 @@ private slots: /// void closedConnection(void); - /// forward message to all json slaves - void forwardJsonMessage(const QJsonObject &message); - public slots: - /// process current forwarder state - void componentStateChanged(const hyperion::Components component, bool enable); - /// - /// forward message to a single json slaves + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settings type from enum + /// @param config configuration object /// - /// @param message The JSON message to send - /// - void sendMessage(const QJsonObject & message, QTcpSocket * socket); + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); private: /// The TCP server object - QTcpServer _server; - - /// Link to Hyperion to get config state emiter - Hyperion * _hyperion; + QTcpServer * _server; /// List with open connections QSet _openConnections; @@ -72,6 +66,13 @@ private: /// the logger instance Logger * _log; - /// Flag if forwarder is enabled - bool _forwarder_enabled = true; + NetOrigin* _netOrigin; + + /// port + uint16_t _port = 0; + + BonjourServiceRegister * _serviceRegister = nullptr; + + void start(); + void stop(); }; diff --git a/include/leddevice/LedDevice.h b/include/leddevice/LedDevice.h index 27218e5a..485f537c 100644 --- a/include/leddevice/LedDevice.h +++ b/include/leddevice/LedDevice.h @@ -34,11 +34,8 @@ class LedDevice : public QObject Q_OBJECT public: - LedDevice(); - /// - /// Empty virtual destructor for pure virtual base class - /// - virtual ~LedDevice() {} + LedDevice(const QJsonObject& config = QJsonObject(), QObject* parent = nullptr); + virtual ~LedDevice(); /// Switch the leds off (led hardware disable) virtual int switchOff(); @@ -49,26 +46,37 @@ public: virtual int setLedValues(const std::vector& ledValues); /// - /// Opens and configures the output device + /// @brief Get color order of device + /// @return The color order /// - /// @return Zero on succes else negative - /// - virtual int open(); + const QString & getColorOrder() { return _colorOrder; }; - static int addToDeviceMap(QString name, LedDeviceCreateFuncType funcPtr); - static const LedDeviceRegistry& getDeviceMap(); - static void setActiveDevice(QString dev); - static QString activeDevice() { return _activeDevice; } - static QJsonObject getLedDeviceSchemas(); - static void setLedCount(int ledCount); - static int getLedCount() { return _ledCount; } + void setActiveDevice(QString dev); + const QString & getActiveDevice() { return _activeDevice; }; + void setLedCount(int ledCount); + int getLedCount() { return _ledCount; } void setEnable(bool enable); bool enabled() { return _enabled; }; - int getLatchTime() { return _latchTime_ms; }; + const int getLatchTime() { return _latchTime_ms; }; inline bool componentState() { return enabled(); }; +public slots: + /// + /// Is called on thread start, all construction tasks and init should run here + /// + virtual void start() { _deviceReady = open(); }; + + /// + /// Writes the RGB-Color values to the leds. + /// + /// @param[in] ledValues The RGB-color per led + /// + /// @return Zero on success else negative + /// + virtual int write(const std::vector& ledValues) = 0; + signals: /// /// Emits whenever the led device switches between on/off @@ -77,16 +85,18 @@ signals: void enableStateChanged(bool newState); protected: - /// - /// Writes the RGB-Color values to the leds. - /// - /// @param[in] ledValues The RGB-color per led - /// - /// @return Zero on success else negative - /// - virtual int write(const std::vector& ledValues) = 0; virtual bool init(const QJsonObject &deviceConfig); + /// + /// Opens and configures the output device + /// + /// @return Zero on succes else negative + /// + virtual int open(); + + // Helper to pipe device config from constructor to start() + QJsonObject _devConfig; + /// The common Logger instance for all LedDevices Logger * _log; @@ -95,12 +105,11 @@ protected: bool _deviceReady; - static QString _activeDevice; - static LedDeviceRegistry _ledDeviceMap; + QString _activeDevice; - static int _ledCount; - static int _ledRGBCount; - static int _ledRGBWCount; + int _ledCount; + int _ledRGBCount; + int _ledRGBWCount; /// Timer object which makes sure that led data is written at a minimum rate /// e.g. Adalight device will switch off when it does not receive data at least every 15 seconds @@ -116,4 +125,5 @@ private: std::vector _ledValues; bool _componentRegistered; bool _enabled; + QString _colorOrder; }; diff --git a/include/leddevice/LedDeviceFactory.h b/include/leddevice/LedDeviceFactory.h index 29691a98..5a26a8ed 100644 --- a/include/leddevice/LedDeviceFactory.h +++ b/include/leddevice/LedDeviceFactory.h @@ -4,7 +4,6 @@ // Leddevice includes #include - /// /// The LedDeviceFactory is responsible for constructing 'LedDevices' /// @@ -20,5 +19,5 @@ public: /// @return The constructed LedDevice or nullptr if configuration is invalid. The ownership of /// the constructed LedDevice is tranferred to the caller /// - static LedDevice * construct(const QJsonObject & deviceConfig, const int ledCount); + static LedDevice * construct(const QJsonObject & deviceConfig); }; diff --git a/include/leddevice/LedDeviceWrapper.h b/include/leddevice/LedDeviceWrapper.h new file mode 100644 index 00000000..b3cb9271 --- /dev/null +++ b/include/leddevice/LedDeviceWrapper.h @@ -0,0 +1,111 @@ +#pragma once + +// util +#include +#include +#include + +class LedDevice; +class Hyperion; + +typedef LedDevice* ( *LedDeviceCreateFuncType ) ( const QJsonObject& ); +typedef std::map LedDeviceRegistry; + +/// +/// @brief Creates and destroys LedDevice instances with LedDeviceFactory and moves the device to a thread. Pipes all signal/slots and methods to LedDevice instance +/// +class LedDeviceWrapper : public QObject +{ + Q_OBJECT +public: + LedDeviceWrapper(Hyperion* hyperion); + ~LedDeviceWrapper(); + /// + /// @brief Contructs a new LedDevice, moves to thread and starts + /// @param config With the given config + /// + void createLedDevice(const QJsonObject& config); + + /// + /// @brief Get all available device schemas + /// @return device schemas + /// + static const QJsonObject getLedDeviceSchemas(); + + /// + /// @brief add all device constrcutors to the map + /// + static int addToDeviceMap(QString name, LedDeviceCreateFuncType funcPtr); + + /// + /// @brief Return all available device contructors + /// @return device constrcutors + /// + static const LedDeviceRegistry& getDeviceMap(); + + /// + /// @brief Get the current latchtime of the ledDevice + /// @ return latchtime in ms + /// + int getLatchTime(); + + /// + /// @brief Get the current active ledDevice + /// + const QString & getActiveDevice(); + + /// + /// @brief Return the last enable state + /// + const bool & enabled() { return _enabled; }; + + /// + /// @brief Get the current colorOrder from device + /// + const QString & getColorOrder(); + +public slots: + /// + /// @brief Handle new component state request + /// @apram component The comp from enum + /// @param state The new state + /// + void handleComponentState(const hyperion::Components component, const bool state); + +signals: + /// + /// PIPER signal for Hyperion -> LedDevice + /// + /// @param[in] ledValues The RGB-color per led + /// + /// @return Zero on success else negative + /// + int write(const std::vector& ledValues); + +private slots: + /// + /// @brief Is called whenever the led device switches between on/off. The led device can disable it's component state + /// The signal comes from the LedDevice + /// @param newState The new state of the device + /// + void handleInternalEnableState(bool newState); + + +protected: + /// contains all available led device constrcutors + static LedDeviceRegistry _ledDeviceMap; + +private: + /// + /// @brief switchOff() the device and Stops the device thread + /// + void stopDeviceThread(); + +private: + // parent Hyperion + Hyperion* _hyperion; + // Pointer of current led device + LedDevice* _ledDevice; + // the enable state + bool _enabled; +}; diff --git a/include/protoserver/ProtoConnection.h b/include/protoserver/ProtoConnection.h deleted file mode 100644 index 7da5d742..00000000 --- a/include/protoserver/ProtoConnection.h +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -// Qt includes -#include -#include -#include -#include -#include -#include - -// hyperion util -#include -#include -#include -#include - -#include - -/// -/// Connection class to setup an connection to the hyperion server and execute commands -/// -class ProtoConnection : public QObject -{ - - Q_OBJECT - -public: - /// - /// Constructor - /// - /// @param address The address of the Hyperion server (for example "192.168.0.32:19444) - /// - ProtoConnection(const QString & address); - - /// - /// Destructor - /// - ~ProtoConnection(); - - /// Do not read reply messages from Hyperion if set to true - void setSkipReply(bool skip); - - /// - /// Set all leds to the specified color - /// - /// @param color The color - /// @param priority The priority - /// @param duration The duration in milliseconds - /// - void setColor(const ColorRgb & color, int priority, int duration = 1); - - /// - /// Set the leds according to the given image (assume the image is stretched to the display size) - /// - /// @param image The image - /// @param priority The priority - /// @param duration The duration in milliseconds - /// - void setImage(const Image & image, int priority, int duration = -1); - - /// - /// Clear the given priority channel - /// - /// @param priority The priority - /// - void clear(int priority); - - /// - /// Clear all priority channels - /// - void clearAll(); - - /// - /// Send a command message and receive its reply - /// - /// @param message The message to send - /// - void sendMessage(const proto::HyperionRequest & message); - -private slots: - /// Try to connect to the Hyperion host - void connectToHost(); - - /// - /// Slot called when new data has arrived - /// - void readData(); - -signals: - - /// - /// emits when a new videoMode was requested from proto client - /// - void setVideoMode(const VideoMode videoMode); - -private: - - /// - /// Parse a reply message - /// - /// @param reply The received reply - /// - /// @return true if the reply indicates success - /// - bool parseReply(const proto::HyperionReply & reply); - -private: - /// The TCP-Socket with the connection to the server - QTcpSocket _socket; - - /// Host address - QString _host; - - /// Host port - uint16_t _port; - - /// Skip receiving reply messages from Hyperion if set - bool _skipReply; - - QTimer _timer; - QAbstractSocket::SocketState _prevSocketState; - - /// The buffer used for reading data from the socket - QByteArray _receiveBuffer; - - Logger * _log; -}; diff --git a/include/protoserver/ProtoConnectionWrapper.h b/include/protoserver/ProtoConnectionWrapper.h deleted file mode 100644 index 87646e52..00000000 --- a/include/protoserver/ProtoConnectionWrapper.h +++ /dev/null @@ -1,41 +0,0 @@ -// Qt includes -#include - -// hyperion includes -#include -#include -#include - -// hyperion proto includes -#include "protoserver/ProtoConnection.h" - -/// This class handles callbacks from the V4L2 and X11 grabber -class ProtoConnectionWrapper : public QObject -{ - Q_OBJECT - -public: - ProtoConnectionWrapper(const QString &address, int priority, int duration_ms, bool skipProtoReply); - virtual ~ProtoConnectionWrapper(); - -signals: - /// - /// Forwarding new videoMode - /// - void setVideoMode(const VideoMode videoMode); - -public slots: - /// Handle a single image - /// @param image The image to process - void receiveImage(const Image & image); - -private: - /// Priority for calls to Hyperion - const int _priority; - - /// Duration for color calls to Hyperion - const int _duration_ms; - - /// Hyperion proto connection object - ProtoConnection _connection; -}; diff --git a/include/protoserver/ProtoServer.h b/include/protoserver/ProtoServer.h deleted file mode 100644 index 12daf1c8..00000000 --- a/include/protoserver/ProtoServer.h +++ /dev/null @@ -1,95 +0,0 @@ -#pragma once - -// system includes -#include - -// Qt includes -#include -#include -#include -#include - -// Hyperion includes -#include - -// hyperion includes -#include -#include -#include -#include - -// forward decl -class ProtoClientConnection; -class ProtoConnection; - -namespace proto { -class HyperionRequest; -} - -/// -/// This class creates a TCP server which accepts connections wich can then send -/// in Protocol Buffer encoded commands. This interface to Hyperion is used by -/// hyperion-remote to control the leds -/// -class ProtoServer : public QObject -{ - Q_OBJECT - -public: - /// - /// ProtoServer constructor - /// @param hyperion Hyperion instance - /// @param port port number on which to start listening for connections - /// - ProtoServer(uint16_t port = 19445); - ~ProtoServer(); - - /// - /// @return the port number on which this TCP listens for incoming connections - /// - uint16_t getPort() const; - -public slots: - void sendImageToProtoSlaves(int priority, const Image & image, int duration_ms); - void componentStateChanged(const hyperion::Components component, bool enable); - -signals: - /// - /// Forwarding videoMode - /// - void videoMode(const VideoMode VideoMode); - -private slots: - /// - /// Slot which is called when a client tries to create a new connection - /// - void newConnection(); - - /// - /// Slot which is called when a client closes a connection - /// @param connection The Connection object which is being closed - /// - void closedConnection(ProtoClientConnection * connection); - - void newMessage(const proto::HyperionRequest * message); - -private: - /// Hyperion instance - Hyperion * _hyperion; - - /// The TCP server object - QTcpServer _server; - - /// List with open connections - QSet _openConnections; - QStringList _forwardClients; - - /// Hyperion proto connection object for forwarding - QList _proxy_connections; - - /// Logger instance - Logger * _log; - - /// flag if forwarder is enabled - bool _forwarder_enabled; -}; diff --git a/include/python/PythonInit.h b/include/python/PythonInit.h new file mode 100644 index 00000000..97b0cb16 --- /dev/null +++ b/include/python/PythonInit.h @@ -0,0 +1,13 @@ +#pragma once + +/// +/// @brief Handle the PythonInit, module registers and DeInit +/// +class PythonInit +{ +private: + friend class HyperionDaemon; + + PythonInit(); + ~PythonInit(); +}; diff --git a/include/python/PythonUtils.h b/include/python/PythonUtils.h new file mode 100644 index 00000000..e7cd08b4 --- /dev/null +++ b/include/python/PythonUtils.h @@ -0,0 +1,15 @@ +#pragma once + +#undef slots +#include +#define slots + +// decl +extern PyThreadState* mainThreadState; + +// py path seperator +#ifdef TARGET_WINDOWS + #define PY_PATH_SEP ";"; +#else // not windows + #define PY_PATH_SEP ":"; +#endif diff --git a/include/ssdp/SSDPDiscover.h b/include/ssdp/SSDPDiscover.h new file mode 100644 index 00000000..f3d669a2 --- /dev/null +++ b/include/ssdp/SSDPDiscover.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +class QUdpSocket; + +enum searchType{ + STY_WEBSERVER, + STY_FLATBUFSERVER +}; + +/// +/// @brief Search for SSDP sessions, used by standalone capture binaries +/// +class SSDPDiscover : public QObject +{ + Q_OBJECT + +public: + SSDPDiscover(QObject* parent = nullptr); + + /// + /// @brief Search for specified service, results will be returned by signal newService(). Calling this method again will reset all found usns and search again + /// @param st The service to search for + /// + void searchForService(const QString& st = "urn:hyperion-project.org:device:basic:1"); + + /// + /// @brief Search for specified searchTarget, the method will block until a server has been found or a timeout happend + /// @param type The address type one of struct searchType + /// @param st The service to search for + /// @param timeout_ms The timeout in ms + /// @return The address+port of webserver or empty if timed out + /// + const QString getFirstService(const searchType& type = STY_WEBSERVER,const QString& st = "urn:hyperion-project.org:device:basic:1", const int& timeout_ms = 3000); + +signals: + /// + /// @brief Emits whenever a new service has ben found, search started with searchForService() + /// @param webServer The address+port of webserver "192.168.0.10:8090" + /// + void newService(const QString webServer); + +private slots: + void readPendingDatagrams(); + +private: + void sendSearch(const QString& st); + +private: + Logger* _log; + QUdpSocket* _udpSocket; + QString _searchTarget; + QStringList _usnList; +}; diff --git a/include/ssdp/SSDPHandler.h b/include/ssdp/SSDPHandler.h new file mode 100644 index 00000000..abd60cec --- /dev/null +++ b/include/ssdp/SSDPHandler.h @@ -0,0 +1,81 @@ +#pragma once + +#include + +#include + +// utils +#include + +class WebServer; +class QNetworkConfigurationManager; + +/// +/// Manage SSDP discovery. SimpleServiceDisoveryProtocol is the discovery subset of UPnP. Implemented is spec V1.0. +/// As SSDP requires a webserver, this class depends on it +/// UPnP 1.0: spec: http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf +/// + +class SSDPHandler : public SSDPServer{ + Q_OBJECT +public: + SSDPHandler(WebServer* webserver, const quint16& flatBufPort, QObject * parent = nullptr); + +public slots: + /// + /// @brief Init SSDP after thread start + /// + void initServer(); + + /// + /// @brief get state changes from webserver + /// + void handleWebServerStateChange(const bool newState); + + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + +private: + /// + /// @brief Build http url for current ip:port/desc.xml + /// + const QString getDescAddress(); + + /// + /// @brief Get the base address + /// + const QString getBaseAddress(); + + /// + /// @brief Build the ssdp description (description.xml) + /// + const QString buildDesc(); + + /// + /// @brief Get the local address of interface + /// @return the address, might be empty + /// + const QString getLocalAddress(); + +private slots: + /// + /// @brief Handle the mSeach request from SSDPServer + /// @param target The ST service type + /// @param mx Answer with delay in s + /// @param address The ip of the caller + /// @param port The port of the caller + /// + void handleMSearchRequest(const QString& target, const QString& mx, const QString address, const quint16 & port); + + void handleNetworkConfigurationChanged(const QNetworkConfiguration &config); + +private: + WebServer* _webserver; + QString _localAddress; + QNetworkConfigurationManager* _NCA; + quint16 _flatbufPort; +}; diff --git a/include/ssdp/SSDPServer.h b/include/ssdp/SSDPServer.h new file mode 100644 index 00000000..71fe02b7 --- /dev/null +++ b/include/ssdp/SSDPServer.h @@ -0,0 +1,98 @@ +#pragma once + +#include + +class QUdpSocket; + +/// +/// @brief The SSDP Server sends and receives (parses) SSDP requests +/// +class SSDPServer : public QObject { + Q_OBJECT + +public: + friend class SSDPHandler; + /// + /// @brief Construct the server, listen on default ssdp address/port with multicast + /// @param parent The parent object + /// + SSDPServer(QObject* parent = nullptr); + virtual ~SSDPServer(); + + /// + /// @brief Prepare server after thread start + /// + void initServer(); + + /// + /// @brief Start SSDP + /// @return false if already running or bind failure + /// + const bool start(); + + /// + /// @brief Stop SSDP + /// + void stop(); + + /// + /// @brief Send an answer to mSearch requester + /// @param st the searchTarget + /// @param senderIp Ip address of the sender + /// @param senderPort The port of the sender + /// + void sendMSearchResponse(const QString& st, const QString& senderIp, const quint16& senderPort); + + /// + /// @brief Send ByeBye notification (on SSDP stop) (repeated 3 times) + /// @param st Search target + /// + void sendByeBye(const QString& st); + + /// + /// @brief Send a NOTIFY msg on SSDP startup to notify our presence (repeated 3 times) + /// @param st The search target + /// + void sendAlive(const QString& st); + + /// + /// @brief Send a NOTIFY msg as ssdp:update to notify about changes + /// @param st The search target + /// + void sendUpdate(const QString& st); + + /// + /// @brief Overwrite description address + /// @param addr new address + /// + void setDescriptionAddress(const QString& addr) { _descAddress = addr; }; + + /// + /// @brief set new flatbuffer server port + /// + void setFlatBufPort(const quint16& port) { _fbsPort = QString::number(port); }; + +signals: + /// + /// @brief Emits whenever a new SSDP search "man : ssdp:discover" is received along with the service type + /// @param target The ST service type + /// @param mx Answer with delay in s + /// @param address The ip of the caller + /// @param port The port of the caller + /// + void msearchRequestReceived(const QString& target, const QString& mx, const QString address, const quint16 & port); + +private: + Logger* _log; + QUdpSocket* _udpSocket; + + QString _serverHeader; + QString _uuid; + QString _fbsPort; + QString _descAddress; + bool _running; + +private slots: + void readPendingDatagrams(); + +}; diff --git a/include/udplistener/UDPListener.h b/include/udplistener/UDPListener.h index 2e7c9e64..83b50599 100644 --- a/include/udplistener/UDPListener.h +++ b/include/udplistener/UDPListener.h @@ -4,16 +4,20 @@ #include // Qt includes -#include #include #include +#include // Hyperion includes -#include #include #include +#include -class UDPClientConnection; +// settings +#include + +class BonjourServiceRegister; +class QUdpSocket; /// /// This class creates a UDP server which accepts connections from boblight clients. @@ -28,26 +32,25 @@ public: /// @param hyperion Hyperion instance /// @param port port number on which to start listening for connections /// - UDPListener(const int priority, const int timeout, const QString& address, quint16 listenPort, bool shared); + UDPListener(const QJsonDocument& config); ~UDPListener(); /// /// @return the port number on which this UDP listens for incoming connections /// uint16_t getPort() const; - + /// /// @return true if server is active (bind to a port) /// bool active() { return _isActive; }; - bool componentState() { return active(); }; public slots: /// /// bind server to network /// void start(); - + /// /// close server /// @@ -55,8 +58,23 @@ public slots: void componentStateChanged(const hyperion::Components component, bool enable); + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + signals: - void statusChanged(bool isActive); + /// + /// @brief forward register data to HyperionDaemon + /// + void registerGlobalInput(const int priority, const hyperion::Components& component, const QString& origin = "System", const QString& owner = "", unsigned smooth_cfg = 0); + + /// + /// @brief forward led data to HyperionDaemon + /// + const bool setGlobalInput(const int priority, const std::vector& ledColors, const int timeout_ms = -1, const bool& clearEffect = true); private slots: /// @@ -66,15 +84,10 @@ private slots: void processTheDatagram(const QByteArray * datagram, const QHostAddress * sender); private: - /// Hyperion instance - Hyperion * _hyperion; /// The UDP server object QUdpSocket * _server; - /// List with open connections - QSet _openConnections; - /// hyperion priority int _priority; @@ -83,12 +96,15 @@ private: /// Logger instance Logger * _log; - + + /// Bonjour Service Register + BonjourServiceRegister* _serviceRegister = nullptr; + /// state of connection bool _isActive; - + /// address to bind QHostAddress _listenAddress; - quint16 _listenPort; + uint16_t _listenPort; QAbstractSocket::BindFlag _bondage; }; diff --git a/include/utils/Components.h b/include/utils/Components.h index 6af45394..ba2350b6 100644 --- a/include/utils/Components.h +++ b/include/utils/Components.h @@ -3,12 +3,14 @@ namespace hyperion { + /** * Enumeration of components in Hyperion. */ enum Components { COMP_INVALID, + COMP_ALL, COMP_SMOOTHING, COMP_BLACKBORDER, COMP_FORWARDER, @@ -17,15 +19,17 @@ enum Components COMP_GRABBER, COMP_V4L, COMP_COLOR, + COMP_IMAGE, COMP_EFFECT, - COMP_PROTOSERVER, - COMP_LEDDEVICE + COMP_LEDDEVICE, + COMP_FLATBUFSERVER }; inline const char* componentToString(Components c) { switch (c) { + case COMP_ALL: return "Hyperion"; case COMP_SMOOTHING: return "Smoothing"; case COMP_BLACKBORDER: return "Blackborder detector"; case COMP_FORWARDER: return "Json/Proto forwarder"; @@ -35,8 +39,9 @@ inline const char* componentToString(Components c) case COMP_V4L: return "V4L capture device"; case COMP_COLOR: return "Solid color"; case COMP_EFFECT: return "Effect"; - case COMP_PROTOSERVER: return "Proto Server"; + case COMP_IMAGE: return "Image"; case COMP_LEDDEVICE: return "LED device"; + case COMP_FLATBUFSERVER: return "Image Receiver"; default: return ""; } } @@ -45,6 +50,7 @@ inline const char* componentToIdString(Components c) { switch (c) { + case COMP_ALL: return "ALL"; case COMP_SMOOTHING: return "SMOOTHING"; case COMP_BLACKBORDER: return "BLACKBORDER"; case COMP_FORWARDER: return "FORWARDER"; @@ -54,8 +60,9 @@ inline const char* componentToIdString(Components c) case COMP_V4L: return "V4L"; case COMP_COLOR: return "COLOR"; case COMP_EFFECT: return "EFFECT"; - case COMP_PROTOSERVER: return "PROTOSERVER"; + case COMP_IMAGE: return "IMAGE"; case COMP_LEDDEVICE: return "LEDDEVICE"; + case COMP_FLATBUFSERVER: return "FLATBUFSERVER"; default: return ""; } } @@ -63,6 +70,7 @@ inline const char* componentToIdString(Components c) inline Components stringToComponent(QString component) { component = component.toUpper(); + if (component == "ALL") return COMP_ALL; if (component == "SMOOTHING") return COMP_SMOOTHING; if (component == "BLACKBORDER") return COMP_BLACKBORDER; if (component == "FORWARDER") return COMP_FORWARDER; @@ -72,9 +80,9 @@ inline Components stringToComponent(QString component) if (component == "V4L") return COMP_V4L; if (component == "COLOR") return COMP_COLOR; if (component == "EFFECT") return COMP_EFFECT; - if (component == "PROTOSERVER") return COMP_PROTOSERVER; + if (component == "IMAGE") return COMP_IMAGE; if (component == "LEDDEVICE") return COMP_LEDDEVICE; - + if (component == "FLATBUFSERVER") return COMP_FLATBUFSERVER; return COMP_INVALID; } diff --git a/include/utils/FileUtils.h b/include/utils/FileUtils.h index 52a7e569..e0d29e13 100644 --- a/include/utils/FileUtils.h +++ b/include/utils/FileUtils.h @@ -13,6 +13,11 @@ namespace FileUtils { QString getBaseName( QString sourceFile); QString getDirName( QString sourceFile); + /// + /// @brief remove directory recursive given by path + /// @param[in] path Path to directory + bool removeDir(const QString& path, Logger* log); + /// /// @brief check if the file exists /// @param[in] path The file path to check @@ -45,9 +50,10 @@ QString getDirName( QString sourceFile); /// @brief delete a file by given path /// @param[in] path The file path to delete /// @param[in] log The logger of the caller to print errors + /// @param[in] ignError Ignore errors during file delete (no log output) /// @return true on success else false /// - bool removeFile(const QString& path, Logger* log); + bool removeFile(const QString& path, Logger* log, bool ignError=false); /// /// @brief Convert a path that may contain special placeholders diff --git a/include/utils/GlobalSignals.h b/include/utils/GlobalSignals.h new file mode 100644 index 00000000..e3fd8efd --- /dev/null +++ b/include/utils/GlobalSignals.h @@ -0,0 +1,41 @@ +#pragma once + +// util +#include +#include + +// qt +#include + +/// +/// Singleton instance for simple signal sharing across threads, should be never used with Qt:DirectConnection! +/// +class GlobalSignals : public QObject +{ + Q_OBJECT +public: + static GlobalSignals* getInstance() + { + static GlobalSignals instance; + return & instance; + } +private: + GlobalSignals() {} + +public: + GlobalSignals(GlobalSignals const&) = delete; + void operator=(GlobalSignals const&) = delete; + +signals: + /// + /// @brief PIPE SystemCapture images from GrabberWrapper to Hyperion class + /// @param image The prepared image + /// + void setSystemImage(const Image& image); + + /// + /// @brief PIPE v4lCapture images from v4lCapture over HyperionDaemon to Hyperion class + /// @param image The prepared image + /// + void setV4lImage(const Image & image); +}; diff --git a/include/utils/Image.h b/include/utils/Image.h index d2c427e5..369659b7 100644 --- a/include/utils/Image.h +++ b/include/utils/Image.h @@ -71,6 +71,38 @@ public: memcpy(_pixels, other._pixels, other._width * other._height * sizeof(Pixel_T)); } + // Define assignment operator in terms of the copy constructor + // More to read: https://stackoverflow.com/questions/255612/dynamically-allocating-an-array-of-objects?answertab=active#tab-top + Image& operator=(Image rhs) + { + rhs.swap(*this); + return *this; + } + + void swap(Image& s) noexcept + { + using std::swap; + swap(this->_width, s._width); + swap(this->_height, s._height); + swap(this->_pixels, s._pixels); + swap(this->_endOfPixels, s._endOfPixels); + } + + // C++11 + Image(Image&& src) noexcept + : _width(0) + , _height(0) + , _pixels(NULL) + , _endOfPixels(NULL) + { + src.swap(*this); + } + Image& operator=(Image&& src) noexcept + { + src.swap(*this); + return *this; + } + /// /// Destructor /// @@ -208,7 +240,7 @@ public: /// /// get size of buffer // - ssize_t size() + ssize_t size() const { return _width * _height * sizeof(Pixel_T); } diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h index 6187b226..4f55475d 100644 --- a/include/utils/JsonUtils.h +++ b/include/utils/JsonUtils.h @@ -26,7 +26,7 @@ namespace JsonUtils{ bool readSchema(const QString& path, QJsonObject& obj, Logger* log); /// - /// @brief parse a json QString and get the result on success + /// @brief parse a json QString and get a QJsonObject. Overloaded funtion /// @param[in] path The file path/name just used for log messages /// @param[in] data Data to parse /// @param[out] obj Retuns the parsed QJsonObject @@ -35,6 +35,26 @@ namespace JsonUtils{ /// bool parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log); + /// + /// @brief parse a json QString and get a QJsonArray. Overloaded function + /// @param[in] path The file path/name just used for log messages + /// @param[in] data Data to parse + /// @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); + + /// + /// @brief parse a json QString and get a QJsonDocument + /// @param[in] path The file path/name just used for log messages + /// @param[in] data Data to parse + /// @param[out] doc Retuns the parsed QJsonDocument + /// @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); + /// /// @brief Validate json data against a schema /// @param[in] file The path/name of json file just used for log messages @@ -45,6 +65,16 @@ namespace JsonUtils{ /// bool validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log); + /// + /// @brief Validate json data against a schema + /// @param[in] file The path/name of json file just used for log messages + /// @param[in] json The json data + /// @param[in] schema The schema object + /// @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); + /// /// @brief Write json data to file /// @param[in] filenameThe file path to write diff --git a/include/utils/NetUtils.h b/include/utils/NetUtils.h new file mode 100644 index 00000000..1678947a --- /dev/null +++ b/include/utils/NetUtils.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include + +namespace NetUtils { + /// + /// @brief Check if the port is available for listening + /// @param[in/out] port The port to test, will be incremented if port is in use + /// @param log The logger of the caller to print + /// @return True on success else false + /// + static const bool portAvailable(quint16& port, Logger* log) + { + const quint16 prevPort = port; + QTcpServer server; + bool corrected = false; + while (!server.listen(QHostAddress::Any, port)) + { + corrected = true; + Warning(log,"Port '%d' is already in use, will increment", port); + port ++; + } + server.close(); + if(corrected) + { + Warning(log, "The requested Port '%d' was already in use, will use Port '%d' instead", prevPort, port); + return false; + } + return true; + } + +} diff --git a/include/utils/Stats.h b/include/utils/Stats.h index d2c2556c..7263f50b 100644 --- a/include/utils/Stats.h +++ b/include/utils/Stats.h @@ -16,6 +16,14 @@ class Stats : public QObject public: Stats(); + static Stats* getInstance() { return instance; }; + static Stats* instance; + + void handleDataUpdate(const QJsonObject& config); + +private: + friend class HyperionDaemon; + Stats(const QJsonObject& config); ~Stats(); private: diff --git a/include/utils/hyperion.h b/include/utils/hyperion.h new file mode 100644 index 00000000..d70263cb --- /dev/null +++ b/include/utils/hyperion.h @@ -0,0 +1,322 @@ +#pragma once + +#include + +#include +#include +#include +// fg effect +#include + +/// +/// @brief Provide utility methods for Hyperion class +/// +namespace hyperion { + + void handleInitialEffect(Hyperion* hyperion, const QJsonObject& FGEffectConfig) + { + #define FGCONFIG_ARRAY fgColorConfig.toArray() + const int FG_PRIORITY = 0; + const int DURATION_INFINITY = 0; + + // initial foreground effect/color + if (FGEffectConfig["enable"].toBool(true)) + { + const QString fgTypeConfig = FGEffectConfig["type"].toString("effect"); + const QString fgEffectConfig = FGEffectConfig["effect"].toString("Rainbow swirl fast"); + const QJsonValue fgColorConfig = FGEffectConfig["color"]; + int default_fg_duration_ms = 3000; + int fg_duration_ms = FGEffectConfig["duration_ms"].toInt(default_fg_duration_ms); + if (fg_duration_ms == DURATION_INFINITY) + { + fg_duration_ms = default_fg_duration_ms; + Warning(Logger::getInstance("HYPERION"), "foreground effect duration 'infinity' is forbidden, set to default value %d ms",default_fg_duration_ms); + } + if ( fgTypeConfig.contains("color") ) + { + ColorRgb fg_color = { + (uint8_t)FGCONFIG_ARRAY.at(0).toInt(0), + (uint8_t)FGCONFIG_ARRAY.at(1).toInt(0), + (uint8_t)FGCONFIG_ARRAY.at(2).toInt(0) + }; + hyperion->setColor(FG_PRIORITY, fg_color, fg_duration_ms); + Info(Logger::getInstance("HYPERION"),"Inital foreground color set (%d %d %d)",fg_color.red,fg_color.green,fg_color.blue); + } + else + { + int result = hyperion->setEffect(fgEffectConfig, FG_PRIORITY, fg_duration_ms); + Info(Logger::getInstance("HYPERION"),"Inital foreground effect '%s' %s", QSTRING_CSTR(fgEffectConfig), ((result == 0) ? "started" : "failed")); + } + } + #undef FGCONFIG_ARRAY + } + + ColorOrder createColorOrder(const QJsonObject &deviceConfig) + { + return stringToColorOrder(deviceConfig["colorOrder"].toString("rgb")); + } + + RgbTransform* createRgbTransform(const QJsonObject& colorConfig) + { + const double backlightThreshold = colorConfig["backlightThreshold"].toDouble(0.0); + const bool backlightColored = colorConfig["backlightColored"].toBool(false); + const double brightness = colorConfig["brightness"].toInt(100); + const double brightnessComp= colorConfig["brightnessCompensation"].toInt(100); + const double gammaR = colorConfig["gammaRed"].toDouble(1.0); + const double gammaG = colorConfig["gammaGreen"].toDouble(1.0); + const double gammaB = colorConfig["gammaBlue"].toDouble(1.0); + + RgbTransform* transform = new RgbTransform(gammaR, gammaG, gammaB, backlightThreshold, backlightColored, brightness, brightnessComp); + return transform; + } + + RgbChannelAdjustment* createRgbChannelAdjustment(const QJsonObject& colorConfig, const QString channelName, const int defaultR, const int defaultG, const int defaultB) + { + const QJsonArray& channelConfig = colorConfig[channelName].toArray(); + RgbChannelAdjustment* adjustment = new RgbChannelAdjustment( + channelConfig[0].toInt(defaultR), + channelConfig[1].toInt(defaultG), + channelConfig[2].toInt(defaultB), + "ChannelAdjust_"+channelName.toUpper() + ); + return adjustment; + } + + ColorAdjustment * createColorAdjustment(const QJsonObject & adjustmentConfig) + { + const QString id = adjustmentConfig["id"].toString("default"); + + RgbChannelAdjustment * blackAdjustment = createRgbChannelAdjustment(adjustmentConfig, "black" , 0, 0, 0); + RgbChannelAdjustment * whiteAdjustment = createRgbChannelAdjustment(adjustmentConfig, "white" , 255,255,255); + RgbChannelAdjustment * redAdjustment = createRgbChannelAdjustment(adjustmentConfig, "red" , 255, 0, 0); + RgbChannelAdjustment * greenAdjustment = createRgbChannelAdjustment(adjustmentConfig, "green" , 0,255, 0); + RgbChannelAdjustment * blueAdjustment = createRgbChannelAdjustment(adjustmentConfig, "blue" , 0, 0,255); + RgbChannelAdjustment * cyanAdjustment = createRgbChannelAdjustment(adjustmentConfig, "cyan" , 0,255,255); + RgbChannelAdjustment * magentaAdjustment = createRgbChannelAdjustment(adjustmentConfig, "magenta", 255, 0,255); + RgbChannelAdjustment * yellowAdjustment = createRgbChannelAdjustment(adjustmentConfig, "yellow" , 255,255, 0); + RgbTransform * rgbTransform = createRgbTransform(adjustmentConfig); + + ColorAdjustment * adjustment = new ColorAdjustment(); + adjustment->_id = id; + adjustment->_rgbBlackAdjustment = *blackAdjustment; + adjustment->_rgbWhiteAdjustment = *whiteAdjustment; + adjustment->_rgbRedAdjustment = *redAdjustment; + adjustment->_rgbGreenAdjustment = *greenAdjustment; + adjustment->_rgbBlueAdjustment = *blueAdjustment; + adjustment->_rgbCyanAdjustment = *cyanAdjustment; + adjustment->_rgbMagentaAdjustment = *magentaAdjustment; + adjustment->_rgbYellowAdjustment = *yellowAdjustment; + adjustment->_rgbTransform = *rgbTransform; + + // Cleanup the allocated individual adjustments + delete blackAdjustment; + delete whiteAdjustment; + delete redAdjustment; + delete greenAdjustment; + delete blueAdjustment; + delete cyanAdjustment; + delete magentaAdjustment; + delete yellowAdjustment; + delete rgbTransform; + + return adjustment; + } + + MultiColorAdjustment * createLedColorsAdjustment(const unsigned ledCnt, const QJsonObject & colorConfig) + { + // Create the result, the transforms are added to this + MultiColorAdjustment * adjustment = new MultiColorAdjustment(ledCnt); + + const QJsonValue adjustmentConfig = colorConfig["channelAdjustment"]; + const QRegExp overallExp("([0-9]+(\\-[0-9]+)?)(,[ ]*([0-9]+(\\-[0-9]+)?))*"); + + const QJsonArray & adjustmentConfigArray = adjustmentConfig.toArray(); + for (signed i = 0; i < adjustmentConfigArray.size(); ++i) + { + const QJsonObject & config = adjustmentConfigArray.at(i).toObject(); + ColorAdjustment * colorAdjustment = createColorAdjustment(config); + adjustment->addAdjustment(colorAdjustment); + + const QString ledIndicesStr = config["leds"].toString("").trimmed(); + if (ledIndicesStr.compare("*") == 0) + { + // Special case for indices '*' => all leds + adjustment->setAdjustmentForLed(colorAdjustment->_id, 0, ledCnt-1); + //Info(_log, "ColorAdjustment '%s' => [0; %d]", QSTRING_CSTR(colorAdjustment->_id), ledCnt-1); + continue; + } + + if (!overallExp.exactMatch(ledIndicesStr)) + { + //Error(_log, "Given led indices %d not correct format: %s", i, QSTRING_CSTR(ledIndicesStr)); + continue; + } + + std::stringstream ss; + const QStringList ledIndexList = ledIndicesStr.split(","); + for (int i=0; i 0) + { + ss << ", "; + } + if (ledIndexList[i].contains("-")) + { + QStringList ledIndices = ledIndexList[i].split("-"); + int startInd = ledIndices[0].toInt(); + int endInd = ledIndices[1].toInt(); + + adjustment->setAdjustmentForLed(colorAdjustment->_id, startInd, endInd); + ss << startInd << "-" << endInd; + } + else + { + int index = ledIndexList[i].toInt(); + adjustment->setAdjustmentForLed(colorAdjustment->_id, index, index); + ss << index; + } + } + //Info(_log, "ColorAdjustment '%s' => [%s]", QSTRING_CSTR(colorAdjustment->_id), ss.str().c_str()); + } + + 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 + */ + LedString createLedString(const QJsonArray& ledConfigArray, const ColorOrder deviceOrder) + { + LedString ledString; + const QString deviceOrderStr = colorOrderToString(deviceOrder); + int maxLedId = ledConfigArray.size(); + + for (signed i = 0; i < ledConfigArray.size(); ++i) + { + const QJsonObject& index = ledConfigArray[i].toObject(); + + Led led; + led.index = index["index"].toInt(); + led.clone = index["clone"].toInt(-1); + if ( led.clone < -1 || led.clone >= maxLedId ) + { + //Warning(_log, "LED %d: clone index of %d is out of range, clone ignored", led.index, led.clone); + led.clone = -1; + } + + if ( led.clone < 0 ) + { + const QJsonObject& hscanConfig = ledConfigArray[i].toObject()["hscan"].toObject(); + const QJsonObject& vscanConfig = ledConfigArray[i].toObject()["vscan"].toObject(); + led.minX_frac = qMax(0.0, qMin(1.0, hscanConfig["minimum"].toDouble())); + led.maxX_frac = qMax(0.0, qMin(1.0, hscanConfig["maximum"].toDouble())); + led.minY_frac = qMax(0.0, qMin(1.0, vscanConfig["minimum"].toDouble())); + led.maxY_frac = qMax(0.0, qMin(1.0, vscanConfig["maximum"].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(index["colorOrder"].toString(deviceOrderStr)); + ledString.leds().push_back(led); + } + } + + // Make sure the leds are sorted (on their indices) + std::sort(ledString.leds().begin(), ledString.leds().end(), [](const Led& lhs, const Led& rhs){ return lhs.index < rhs.index; }); + return ledString; + } + + LedString createLedStringClone(const QJsonArray& ledConfigArray, const ColorOrder deviceOrder) + { + LedString ledString; + const QString deviceOrderStr = colorOrderToString(deviceOrder); + int maxLedId = ledConfigArray.size(); + + for (signed i = 0; i < ledConfigArray.size(); ++i) + { + const QJsonObject& index = ledConfigArray[i].toObject(); + + Led led; + led.index = index["index"].toInt(); + led.clone = index["clone"].toInt(-1); + if ( led.clone < -1 || led.clone >= maxLedId ) + { + //Warning(_log, "LED %d: clone index of %d is out of range, clone ignored", led.index, led.clone); + led.clone = -1; + } + + if ( led.clone >= 0 ) + { + //Debug(_log, "LED %d: clone from led %d", led.index, led.clone); + led.minX_frac = 0; + led.maxX_frac = 0; + led.minY_frac = 0; + led.maxY_frac = 0; + // Get the order of the rgb channels for this led (default is device order) + led.colorOrder = stringToColorOrder(index["colorOrder"].toString(deviceOrderStr)); + + ledString.leds().push_back(led); + } + + } + + // Make sure the leds are sorted (on their indices) + std::sort(ledString.leds().begin(), ledString.leds().end(), [](const Led& lhs, const Led& rhs){ return lhs.index < rhs.index; }); + return ledString; + } + + QSize getLedLayoutGridSize(const QJsonArray& ledConfigArray) + { + std::vector midPointsX; + std::vector midPointsY; + + for (signed i = 0; i < ledConfigArray.size(); ++i) + { + const QJsonObject& index = ledConfigArray[i].toObject(); + + if (index["clone"].toInt(-1) < 0 ) + { + const QJsonObject& hscanConfig = ledConfigArray[i].toObject()["hscan"].toObject(); + const QJsonObject& vscanConfig = ledConfigArray[i].toObject()["vscan"].toObject(); + double minX_frac = qMax(0.0, qMin(1.0, hscanConfig["minimum"].toDouble())); + double maxX_frac = qMax(0.0, qMin(1.0, hscanConfig["maximum"].toDouble())); + double minY_frac = qMax(0.0, qMin(1.0, vscanConfig["minimum"].toDouble())); + double maxY_frac = qMax(0.0, qMin(1.0, vscanConfig["maximum"].toDouble())); + // Fix if the user swapped min and max + if (minX_frac > maxX_frac) + { + std::swap(minX_frac, maxX_frac); + } + if (minY_frac > maxY_frac) + { + std::swap(minY_frac, maxY_frac); + } + + // calculate mid point and make grid calculation + midPointsX.push_back( int(1000.0*(minX_frac + maxX_frac) / 2.0) ); + midPointsY.push_back( int(1000.0*(minY_frac + maxY_frac) / 2.0) ); + } + } + + // remove duplicates + std::sort(midPointsX.begin(), midPointsX.end()); + midPointsX.erase(std::unique(midPointsX.begin(), midPointsX.end()), midPointsX.end()); + std::sort(midPointsY.begin(), midPointsY.end()); + midPointsY.erase(std::unique(midPointsY.begin(), midPointsY.end()), midPointsY.end()); + + QSize gridSize( midPointsX.size(), midPointsY.size() ); + //Debug(_log, "led layout grid: %dx%d", gridSize.width(), gridSize.height()); + + return gridSize; + } +}; diff --git a/include/utils/settings.h b/include/utils/settings.h new file mode 100644 index 00000000..607a3719 --- /dev/null +++ b/include/utils/settings.h @@ -0,0 +1,99 @@ +#pragma once +#include +#include + +/// +/// @brief Provide util methods to work with SettingsManager class +/// +namespace settings { +// all available settings sections +enum type { + BGEFFECT, + FGEFFECT, + BLACKBORDER, + BOBLSERVER, + COLOR, + DEVICE, + EFFECTS, + NETFORWARD, + SYSTEMCAPTURE, + GENERAL, + V4L2, + JSONSERVER, + LEDCONFIG, + LEDS, + LOGGER, + SMOOTHING, + UDPLISTENER, + WEBSERVER, + INSTCAPTURE, + NETWORK, + FLATBUFSERVER, + INVALID +}; + +/// +/// @brief Convert settings::type to string representation +/// @param type The settings::type from enum +/// @return The settings type as string +/// +inline QString typeToString(const type& type) +{ + switch (type) + { + case BGEFFECT: return "backgroundEffect"; + case FGEFFECT: return "foregroundEffect"; + case BLACKBORDER: return "blackborderdetector"; + case BOBLSERVER: return "boblightServer"; + case COLOR: return "color"; + case DEVICE: return "device"; + case EFFECTS: return "effects"; + case NETFORWARD: return "forwarder"; + case SYSTEMCAPTURE: return "framegrabber"; + case GENERAL: return "general"; + case V4L2: return "grabberV4L2"; + case JSONSERVER: return "jsonServer"; + case LEDCONFIG: return "ledConfig"; + case LEDS: return "leds"; + case LOGGER: return "logger"; + case SMOOTHING: return "smoothing"; + case UDPLISTENER: return "udpListener"; + case WEBSERVER: return "webConfig"; + case INSTCAPTURE: return "instCapture"; + case NETWORK: return "network"; + case FLATBUFSERVER: return "flatbufServer"; + default: return "invalid"; + } +} + +/// +/// @brief Convert string to settings::type representation +/// @param type The string to convert +/// @return The settings type from enum +/// +inline type stringToType(const QString& type) +{ + if (type == "backgroundEffect") return BGEFFECT; + else if (type == "foregroundEffect") return FGEFFECT; + else if (type == "blackborderdetector") return BLACKBORDER; + else if (type == "boblightServer") return BOBLSERVER; + else if (type == "color") return COLOR; + else if (type == "device") return DEVICE; + else if (type == "effects") return EFFECTS; + else if (type == "forwarder") return NETFORWARD; + else if (type == "framegrabber") return SYSTEMCAPTURE; + else if (type == "general") return GENERAL; + else if (type == "grabberV4L2") return V4L2; + else if (type == "jsonServer") return JSONSERVER; + else if (type == "ledConfig") return LEDCONFIG; + else if (type == "leds") return LEDS; + else if (type == "logger") return LOGGER; + else if (type == "smoothing") return SMOOTHING; + else if (type == "udpListener") return UDPLISTENER; + else if (type == "webConfig") return WEBSERVER; + else if (type == "instCapture") return INSTCAPTURE; + else if (type == "network") return NETWORK; + else if (type == "flatbufServer") return FLATBUFSERVER; + else return INVALID; +} +}; diff --git a/include/webconfig/WebConfig.h b/include/webconfig/WebConfig.h deleted file mode 100644 index c9b58bfe..00000000 --- a/include/webconfig/WebConfig.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef WEBCONFIG_H -#define WEBCONFIG_H - -#include -#include -#include - -class StaticFileServing; - -class WebConfig : public QObject { - Q_OBJECT - -public: - WebConfig (QObject * parent = NULL); - - virtual ~WebConfig (void); - - void start(); - void stop(); - - quint16 getPort() { return _port; }; - -private: - Hyperion* _hyperion; - QString _baseUrl; - quint16 _port; - StaticFileServing* _server; - - const QString WEBCONFIG_DEFAULT_PATH = ":/webconfig"; - const quint16 WEBCONFIG_DEFAULT_PORT = 8099; -}; - -#endif // WEBCONFIG_H - diff --git a/include/webserver/WebServer.h b/include/webserver/WebServer.h new file mode 100644 index 00000000..f4aaffc8 --- /dev/null +++ b/include/webserver/WebServer.h @@ -0,0 +1,83 @@ +#ifndef WEBSERVER_H +#define WEBSERVER_H + +#include +#include +#include + +// hyperion / utils +#include + +// settings +#include + +class BonjourServiceRegister; +class StaticFileServing; +class QtHttpServer; + +class WebServer : public QObject { + Q_OBJECT + +public: + WebServer (const QJsonDocument& config, QObject * parent = 0); + + virtual ~WebServer (void); + + void start(); + void stop(); + + quint16 getPort() { return _port; }; + + /// check if server has been inited + const bool isInited() { return _inited; }; + + /// + /// @brief Set a new description, if empty the description is NotFound for clients + /// + void setSSDPDescription(const QString & desc); + +signals: + /// + /// @emits whenever server is started or stopped (to sync with SSDPHandler) + /// @param newState True when started, false when stopped + /// + void stateChange(const bool newState); + + /// + /// @brief Emits whenever the port changes (doesn't compare prev <> now) + /// + void portChanged(const quint16& port); + +public slots: + /// + /// @brief Init server after thread start + /// + void initServer(); + + void onServerStopped (void); + void onServerStarted (quint16 port); + void onServerError (QString msg); + + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + +private: + QJsonDocument _config; + Logger* _log; + QString _baseUrl; + quint16 _port; + StaticFileServing* _staticFileServing; + QtHttpServer* _server; + bool _inited = false; + + const QString WEBSERVER_DEFAULT_PATH = ":/webconfig"; + const quint16 WEBSERVER_DEFAULT_PORT = 8090; + + BonjourServiceRegister * _serviceRegister = nullptr; +}; + +#endif // WEBSERVER_H diff --git a/libsrc/webconfig/WebSocketClient.h b/include/webserver/WebSocketClient.h similarity index 85% rename from libsrc/webconfig/WebSocketClient.h rename to include/webserver/WebSocketClient.h index 7aaf3ce3..b7a273d9 100644 --- a/libsrc/webconfig/WebSocketClient.h +++ b/include/webserver/WebSocketClient.h @@ -1,18 +1,16 @@ #pragma once #include -#include "WebSocketUtils.h" +#include "webserver/WebSocketUtils.h" +#include class QTcpSocket; - -class QtHttpRequest; -class Hyperion; -class JsonProcessor; +class JsonAPI; class WebSocketClient : public QObject { Q_OBJECT public: - WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, QObject* parent); + WebSocketClient(QByteArray socketKey, QTcpSocket* sock, QObject* parent); struct WebSocketHeader { @@ -25,13 +23,13 @@ public: private: QTcpSocket* _socket; + QByteArray _secWebSocketKey; Logger* _log; - Hyperion* _hyperion; - JsonProcessor* _jsonProcessor; + JsonAPI* _jsonAPI; void getWsFrameHeader(WebSocketHeader* header); void sendClose(int status, QString reason = ""); - void handleBinaryMessage(QByteArray &data); +// void handleBinaryMessage(QByteArray &data); qint64 sendMessage_Raw(const char* data, quint64 size); qint64 sendMessage_Raw(QByteArray &data); QByteArray makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame); diff --git a/libsrc/webconfig/WebSocketUtils.h b/include/webserver/WebSocketUtils.h similarity index 100% rename from libsrc/webconfig/WebSocketUtils.h rename to include/webserver/WebSocketUtils.h diff --git a/libsrc/CMakeLists.txt b/libsrc/CMakeLists.txt index c2f15b5a..4ff02b3b 100644 --- a/libsrc/CMakeLists.txt +++ b/libsrc/CMakeLists.txt @@ -7,12 +7,15 @@ add_subdirectory(hyperion) add_subdirectory(commandline) add_subdirectory(blackborder) add_subdirectory(jsonserver) -add_subdirectory(protoserver) +add_subdirectory(flatbufserver) add_subdirectory(bonjour) +add_subdirectory(ssdp) add_subdirectory(boblightserver) add_subdirectory(udplistener) add_subdirectory(leddevice) add_subdirectory(utils) add_subdirectory(effectengine) add_subdirectory(grabber) -add_subdirectory(webconfig) +add_subdirectory(webserver) +add_subdirectory(api) +add_subdirectory(python) diff --git a/libsrc/api/CMakeLists.txt b/libsrc/api/CMakeLists.txt new file mode 100644 index 00000000..64180a9c --- /dev/null +++ b/libsrc/api/CMakeLists.txt @@ -0,0 +1,21 @@ +# 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} +) + +target_link_libraries(hyperion-api + hyperion + hyperion-utils + Qt5::Core + Qt5::Gui + Qt5::Network +) diff --git a/libsrc/utils/JSONRPC_schema/schema-adjustment.json b/libsrc/api/JSONRPC_schema/schema-adjustment.json similarity index 92% rename from libsrc/utils/JSONRPC_schema/schema-adjustment.json rename to libsrc/api/JSONRPC_schema/schema-adjustment.json index a16796b1..c095c09c 100644 --- a/libsrc/utils/JSONRPC_schema/schema-adjustment.json +++ b/libsrc/api/JSONRPC_schema/schema-adjustment.json @@ -84,17 +84,6 @@ "minItems": 3, "maxItems": 3 }, - "black": { - "type": "array", - "required": false, - "items" : { - "type": "integer", - "minimum": 0, - "maximum": 255 - }, - "minItems": 3, - "maxItems": 3 - }, "white": { "type": "array", "required": false, diff --git a/libsrc/utils/JSONRPC_schema/schema-clear.json b/libsrc/api/JSONRPC_schema/schema-clear.json similarity index 94% rename from libsrc/utils/JSONRPC_schema/schema-clear.json rename to libsrc/api/JSONRPC_schema/schema-clear.json index 9d1cb9e5..c63a77f7 100644 --- a/libsrc/utils/JSONRPC_schema/schema-clear.json +++ b/libsrc/api/JSONRPC_schema/schema-clear.json @@ -12,7 +12,7 @@ }, "priority": { "type": "integer", - "minimum" : 1, + "minimum" : -1, "maximum" : 253, "required": true } diff --git a/libsrc/utils/JSONRPC_schema/schema-clearall.json b/libsrc/api/JSONRPC_schema/schema-clearall.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-clearall.json rename to libsrc/api/JSONRPC_schema/schema-clearall.json diff --git a/libsrc/utils/JSONRPC_schema/schema-color.json b/libsrc/api/JSONRPC_schema/schema-color.json similarity index 96% rename from libsrc/utils/JSONRPC_schema/schema-color.json rename to libsrc/api/JSONRPC_schema/schema-color.json index 316cbe80..754c5ad3 100644 --- a/libsrc/utils/JSONRPC_schema/schema-color.json +++ b/libsrc/api/JSONRPC_schema/schema-color.json @@ -24,7 +24,7 @@ "type": "string", "minLength" : 4, "maxLength" : 20, - "required": true + "required": false }, "color": { "type": "array", diff --git a/libsrc/utils/JSONRPC_schema/schema-componentstate.json b/libsrc/api/JSONRPC_schema/schema-componentstate.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-componentstate.json rename to libsrc/api/JSONRPC_schema/schema-componentstate.json diff --git a/libsrc/utils/JSONRPC_schema/schema-config.json b/libsrc/api/JSONRPC_schema/schema-config.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-config.json rename to libsrc/api/JSONRPC_schema/schema-config.json diff --git a/libsrc/utils/JSONRPC_schema/schema-create-effect.json b/libsrc/api/JSONRPC_schema/schema-create-effect.json similarity index 86% rename from libsrc/utils/JSONRPC_schema/schema-create-effect.json rename to libsrc/api/JSONRPC_schema/schema-create-effect.json index 2e29e533..97b55056 100644 --- a/libsrc/utils/JSONRPC_schema/schema-create-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-create-effect.json @@ -24,6 +24,10 @@ { "type" : "object", "required" : true + }, + "imageData" : { + "type" : "string", + "required" : false } }, "additionalProperties": false diff --git a/libsrc/utils/JSONRPC_schema/schema-delete-effect.json b/libsrc/api/JSONRPC_schema/schema-delete-effect.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-delete-effect.json rename to libsrc/api/JSONRPC_schema/schema-delete-effect.json diff --git a/libsrc/utils/JSONRPC_schema/schema-effect.json b/libsrc/api/JSONRPC_schema/schema-effect.json similarity index 89% rename from libsrc/utils/JSONRPC_schema/schema-effect.json rename to libsrc/api/JSONRPC_schema/schema-effect.json index dfd58b59..876173f1 100644 --- a/libsrc/utils/JSONRPC_schema/schema-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-effect.json @@ -24,7 +24,7 @@ "type": "string", "minLength" : 4, "maxLength" : 20, - "required": true + "required": false }, "effect": { "type": "object", @@ -44,6 +44,10 @@ "pythonScript" : { "type" : "string", "required" : false + }, + "imageData" : { + "type" : "string", + "required" : false } }, "additionalProperties": false diff --git a/libsrc/api/JSONRPC_schema/schema-hyperion-classic.json b/libsrc/api/JSONRPC_schema/schema-hyperion-classic.json new file mode 100644 index 00000000..5421b3aa --- /dev/null +++ b/libsrc/api/JSONRPC_schema/schema-hyperion-classic.json @@ -0,0 +1,12 @@ +{ + "type":"object", + "required":true, + "properties":{ + "command": { + "title" : "This schema is used to ensure backward compatibility with hyperion classic", + "type" : "string", + "required" : false + } + }, + "additionalProperties": true +} diff --git a/libsrc/utils/JSONRPC_schema/schema-image.json b/libsrc/api/JSONRPC_schema/schema-image.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-image.json rename to libsrc/api/JSONRPC_schema/schema-image.json diff --git a/libsrc/utils/JSONRPC_schema/schema-ledcolors.json b/libsrc/api/JSONRPC_schema/schema-ledcolors.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-ledcolors.json rename to libsrc/api/JSONRPC_schema/schema-ledcolors.json diff --git a/libsrc/utils/JSONRPC_schema/schema-logging.json b/libsrc/api/JSONRPC_schema/schema-logging.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-logging.json rename to libsrc/api/JSONRPC_schema/schema-logging.json diff --git a/libsrc/utils/JSONRPC_schema/schema-processing.json b/libsrc/api/JSONRPC_schema/schema-processing.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-processing.json rename to libsrc/api/JSONRPC_schema/schema-processing.json diff --git a/libsrc/utils/JSONRPC_schema/schema-serverinfo.json b/libsrc/api/JSONRPC_schema/schema-serverinfo.json similarity index 83% rename from libsrc/utils/JSONRPC_schema/schema-serverinfo.json rename to libsrc/api/JSONRPC_schema/schema-serverinfo.json index 869d9617..990eec04 100644 --- a/libsrc/utils/JSONRPC_schema/schema-serverinfo.json +++ b/libsrc/api/JSONRPC_schema/schema-serverinfo.json @@ -7,6 +7,9 @@ "required" : true, "enum" : ["serverinfo"] }, + "subscribe" : { + "type" : "array" + }, "tan" : { "type" : "integer" } diff --git a/libsrc/utils/JSONRPC_schema/schema-sourceselect.json b/libsrc/api/JSONRPC_schema/schema-sourceselect.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-sourceselect.json rename to libsrc/api/JSONRPC_schema/schema-sourceselect.json diff --git a/libsrc/utils/JSONRPC_schema/schema-sysinfo.json b/libsrc/api/JSONRPC_schema/schema-sysinfo.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-sysinfo.json rename to libsrc/api/JSONRPC_schema/schema-sysinfo.json diff --git a/libsrc/utils/JSONRPC_schema/schema-videomode.json b/libsrc/api/JSONRPC_schema/schema-videomode.json similarity index 100% rename from libsrc/utils/JSONRPC_schema/schema-videomode.json rename to libsrc/api/JSONRPC_schema/schema-videomode.json diff --git a/libsrc/utils/JSONRPC_schema/schema.json b/libsrc/api/JSONRPC_schema/schema.json similarity index 79% rename from libsrc/utils/JSONRPC_schema/schema.json rename to libsrc/api/JSONRPC_schema/schema.json index 9aca8d98..26d77bf9 100644 --- a/libsrc/utils/JSONRPC_schema/schema.json +++ b/libsrc/api/JSONRPC_schema/schema.json @@ -5,7 +5,7 @@ "command": { "type" : "string", "required" : true, - "enum" : ["color", "image", "effect", "create-effect", "delete-effect", "serverinfo", "clear", "clearall", "adjustment", "sourceselect", "config", "componentstate", "ledcolors", "logging", "processing", "sysinfo", "videomode"] + "enum" : ["color", "image", "effect", "create-effect", "delete-effect", "serverinfo", "clear", "clearall", "adjustment", "sourceselect", "config", "componentstate", "ledcolors", "logging", "processing", "sysinfo", "videomode", "transform", "correction" , "temperature"] } } } diff --git a/libsrc/utils/JSONRPC_schemas.qrc b/libsrc/api/JSONRPC_schemas.qrc similarity index 79% rename from libsrc/utils/JSONRPC_schemas.qrc rename to libsrc/api/JSONRPC_schemas.qrc index 1c708443..7595d1ee 100644 --- a/libsrc/utils/JSONRPC_schemas.qrc +++ b/libsrc/api/JSONRPC_schemas.qrc @@ -18,5 +18,9 @@ JSONRPC_schema/schema-logging.json JSONRPC_schema/schema-processing.json JSONRPC_schema/schema-videomode.json + + JSONRPC_schema/schema-hyperion-classic.json + JSONRPC_schema/schema-hyperion-classic.json + JSONRPC_schema/schema-hyperion-classic.json diff --git a/libsrc/utils/JsonProcessor.cpp b/libsrc/api/JsonAPI.cpp similarity index 60% rename from libsrc/utils/JsonProcessor.cpp rename to libsrc/api/JsonAPI.cpp index b1d6dc64..025fd9c1 100644 --- a/libsrc/utils/JsonProcessor.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -1,11 +1,9 @@ // project includes -#include +#include // stl includes #include -#include #include -#include // Qt includes #include @@ -14,62 +12,60 @@ #include #include #include -#include -#include -#include #include +#include +#include // hyperion includes -#include -#include #include #include #include #include -#include +#include #include #include #include +#include + +// bonjour wrapper +#include + +// ledmapping int <> string transform methods +#include + +// api includes +#include using namespace hyperion; -std::map JsonProcessor::_componentsPrevState; - -JsonProcessor::JsonProcessor(QString peerAddress, Logger* log, QObject* parent, bool noListener) +JsonAPI::JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListener) : QObject(parent) + , _noListener(noListener) , _peerAddress(peerAddress) , _log(log) , _hyperion(Hyperion::getInstance()) - , _imageProcessor(ImageProcessorFactory::getInstance().newImageProcessor()) + , _jsonCB(new JsonCB(this)) , _streaming_logging_activated(false) , _image_stream_timeout(0) + , _led_stream_timeout(0) { + Q_INIT_RESOURCE(JSONRPC_schemas); + + // the JsonCB creates json messages you can subscribe to e.g. data change events; forward them to the parent client + connect(_jsonCB, &JsonCB::newCallback, this, &JsonAPI::callbackMessage); + // notify hyperion about a jsonMessageForward - connect(this, &JsonProcessor::forwardJsonMessage, _hyperion, &Hyperion::forwardJsonMessage); - // notify hyperion about a push emit TODO: Remove! Make sure that the target of the commands trigger this (less error margin) instead this instance - connect(this, &JsonProcessor::pushReq, _hyperion, &Hyperion::hyperionStateChanged); - - if(!noListener) - { - // listen for sendServerInfo pushes from hyperion - connect(_hyperion, &Hyperion::sendServerInfo, this, &JsonProcessor::forceServerInfo); - } - - // led color stream update timer - _timer_ledcolors.setSingleShot(false); - connect(&_timer_ledcolors, SIGNAL(timeout()), this, SLOT(streamLedcolorsUpdate())); - _image_stream_mutex.unlock(); + connect(this, &JsonAPI::forwardJsonMessage, _hyperion, &Hyperion::forwardJsonMessage); } -void JsonProcessor::handleMessage(const QString& messageString) +void JsonAPI::handleMessage(const QString& messageString) { const QString ident = "JsonRpc@"+_peerAddress; - Q_INIT_RESOURCE(JSONRPC_schemas); QJsonObject message; // parse the message if(!JsonUtils::parse(ident, messageString, message, _log)) { - sendErrorReply("Errors during message parsing, please consult the Hyperion Log. Data:"+messageString); + sendErrorReply("Errors during message parsing, please consult the Hyperion Log."); return; } @@ -89,6 +85,7 @@ void JsonProcessor::handleMessage(const QString& messageString) } int tan = message["tan"].toInt(); + // switch over all possible commands and handle them if (command == "color") handleColorCommand (message, command, tan); else if (command == "image") handleImageCommand (message, command, tan); @@ -98,7 +95,6 @@ void JsonProcessor::handleMessage(const QString& messageString) 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 == "clearall") handleClearallCommand (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); @@ -107,17 +103,25 @@ void JsonProcessor::handleMessage(const QString& messageString) else if (command == "logging") handleLoggingCommand (message, command, tan); else if (command == "processing") handleProcessingCommand (message, command, tan); else if (command == "videomode") handleVideoModeCommand (message, command, tan); + + // BEGIN | The following commands are derecated 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"); + // END + + // handle not implemented commands else handleNotImplemented (); } -void JsonProcessor::handleColorCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleColorCommand(const QJsonObject& message, const QString& command, const int tan) { emit forwardJsonMessage(message); // extract parameters int priority = message["priority"].toInt(); int duration = message["duration"].toInt(-1); - QString origin = message["origin"].toString() + "@"+_peerAddress; + QString origin = message["origin"].toString("Empty") + "@"+_peerAddress; std::vector colorData(_hyperion->getLedCount()); const QJsonArray & jsonColor = message["color"].toArray(); @@ -143,14 +147,15 @@ void JsonProcessor::handleColorCommand(const QJsonObject& message, const QString memcpy(&(colorData[i]), colorData.data(), (_hyperion->getLedCount()-i) * sizeof(ColorRgb)); } - // set output - _hyperion->setColors(priority, colorData, duration, true, hyperion::COMP_COLOR, origin); + // register and set color + _hyperion->registerInput(priority, hyperion::COMP_COLOR, origin); + _hyperion->setInput(priority, colorData, duration); // send reply sendSuccessReply(command, tan); } -void JsonProcessor::handleImageCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleImageCommand(const QJsonObject& message, const QString& command, const int tan) { emit forwardJsonMessage(message); @@ -168,22 +173,18 @@ void JsonProcessor::handleImageCommand(const QJsonObject& message, const QString return; } - // set width and height of the image processor - _imageProcessor->setSize(width, height); - // create ImageRgb Image image(width, height); memcpy(image.memptr(), data.data(), data.size()); - // process the image - std::vector ledColors = _imageProcessor->process(image); - _hyperion->setColors(priority, ledColors, duration); + _hyperion->registerInput(priority, hyperion::COMP_IMAGE, "JsonRpc@"+_peerAddress); + _hyperion->setInputImage(priority, image, duration); // send reply sendSuccessReply(command, tan); } -void JsonProcessor::handleEffectCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleEffectCommand(const QJsonObject &message, const QString &command, const int tan) { emit forwardJsonMessage(message); @@ -191,128 +192,39 @@ void JsonProcessor::handleEffectCommand(const QJsonObject& message, const QStrin int priority = message["priority"].toInt(); int duration = message["duration"].toInt(-1); QString pythonScript = message["pythonScript"].toString(); - QString origin = message["origin"].toString() + "@"+_peerAddress; + QString origin = message["origin"].toString("Empty") + "@"+_peerAddress; const QJsonObject & effect = message["effect"].toObject(); const QString & effectName = effect["name"].toString(); + const QString & data = message["imageData"].toString("").toUtf8(); // set output - if (effect.contains("args")) - { - _hyperion->setEffect(effectName, effect["args"].toObject(), priority, duration, pythonScript, origin); - } - else - { - _hyperion->setEffect(effectName, priority, duration, origin); - } + (effect.contains("args")) + ? _hyperion->setEffect(effectName, effect["args"].toObject(), priority, duration, pythonScript, origin, data) + : _hyperion->setEffect(effectName, priority, duration, origin); // send reply sendSuccessReply(command, tan); } -void JsonProcessor::handleCreateEffectCommand(const QJsonObject& message, const QString &command, const int tan) +void JsonAPI::handleCreateEffectCommand(const QJsonObject& message, const QString &command, const int tan) { - if (!message["args"].toObject().isEmpty()) - { - QString scriptName; - (message["script"].toString().mid(0, 1) == ":" ) - ? scriptName = ":/effects//" + message["script"].toString().mid(1) - : scriptName = message["script"].toString(); - - std::list effectsSchemas = _hyperion->getEffectSchemas(); - std::list::iterator it = std::find_if(effectsSchemas.begin(), effectsSchemas.end(), find_schema(scriptName)); - - if (it != effectsSchemas.end()) - { - if(!JsonUtils::validate("JsonRpc@"+_peerAddress, message["args"].toObject(), it->schemaFile, _log)) - { - sendErrorReply("Error during arg validation against schema, please consult the Hyperion Log", command, tan); - return; - } - - QJsonObject effectJson; - QJsonArray effectArray; - effectArray = _hyperion->getQJsonConfig()["effects"].toObject()["paths"].toArray(); - - if (effectArray.size() > 0) - { - if (message["name"].toString().trimmed().isEmpty() || message["name"].toString().trimmed().startsWith(".")) - { - sendErrorReply("Can't save new effect. Effect name is empty or begins with a dot.", command, tan); - return; - } - - effectJson["name"] = message["name"].toString(); - effectJson["script"] = message["script"].toString(); - effectJson["args"] = message["args"].toObject(); - - std::list availableEffects = _hyperion->getEffects(); - std::list::iterator iter = std::find_if(availableEffects.begin(), availableEffects.end(), find_effect(message["name"].toString())); - - QFileInfo newFileName; - if (iter != availableEffects.end()) - { - newFileName.setFile(iter->file); - if (newFileName.absoluteFilePath().mid(0, 1) == ":") - { - sendErrorReply("The effect name '" + message["name"].toString() + "' is assigned to an internal effect. Please rename your effekt.", command, tan); - return; - } - } else - { - QString f = FileUtils::convertPath(effectArray[0].toString() + "/" + message["name"].toString().replace(QString(" "), QString("")) + QString(".json")); - newFileName.setFile(f); - } - - if(!JsonUtils::write(newFileName.absoluteFilePath(), effectJson, _log)) - { - sendErrorReply("Error while saving effect, please check the Hyperion Log", command, tan); - return; - } - - Info(_log, "Reload effect list"); - _hyperion->reloadEffects(); - sendSuccessReply(command, tan); - } else - { - sendErrorReply("Can't save new effect. Effect path empty", command, tan); - return; - } - } else - sendErrorReply("Missing schema file for Python script " + message["script"].toString(), command, tan); - } else - sendErrorReply("Missing or empty Object 'args'", command, tan); + QString resultMsg; + if(_hyperion->saveEffect(message, resultMsg)) + sendSuccessReply(command, tan); + else + sendErrorReply(resultMsg, command, tan); } -void JsonProcessor::handleDeleteEffectCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleDeleteEffectCommand(const QJsonObject& message, const QString& command, const int tan) { - QString effectName = message["name"].toString(); - std::list effectsDefinition = _hyperion->getEffects(); - std::list::iterator it = std::find_if(effectsDefinition.begin(), effectsDefinition.end(), find_effect(effectName)); - - if (it != effectsDefinition.end()) - { - QFileInfo effectConfigurationFile(it->file); - if (effectConfigurationFile.absoluteFilePath().mid(0, 1) != ":" ) - { - if (effectConfigurationFile.exists()) - { - bool result = QFile::remove(effectConfigurationFile.absoluteFilePath()); - if (result) - { - Info(_log, "Reload effect list"); - _hyperion->reloadEffects(); - sendSuccessReply(command, tan); - } else - sendErrorReply("Can't delete effect configuration file: " + effectConfigurationFile.absoluteFilePath() + ". Please check permissions", command, tan); - } else - sendErrorReply("Can't find effect configuration file: " + effectConfigurationFile.absoluteFilePath(), command, tan); - } else - sendErrorReply("Can't delete internal effect: " + message["name"].toString(), command, tan); - } else - sendErrorReply("Effect " + message["name"].toString() + " not found", command, tan); + QString resultMsg; + if(_hyperion->deleteEffect(message["name"].toString(), resultMsg)) + sendSuccessReply(command, tan); + else + sendErrorReply(resultMsg, command, tan); } -void JsonProcessor::handleSysInfoCommand(const QJsonObject&, const QString& command, const int tan) +void JsonAPI::handleSysInfoCommand(const QJsonObject&, const QString& command, const int tan) { // create result QJsonObject result; @@ -339,7 +251,7 @@ void JsonProcessor::handleSysInfoCommand(const QJsonObject&, const QString& comm hyperion["version" ] = QString(HYPERION_VERSION); hyperion["build" ] = QString(HYPERION_BUILD_ID); hyperion["time" ] = QString(__DATE__ " " __TIME__); - hyperion["id" ] = _hyperion->id; + hyperion["id" ] = _hyperion->getId(); info["hyperion"] = hyperion; // send the result @@ -347,47 +259,34 @@ void JsonProcessor::handleSysInfoCommand(const QJsonObject&, const QString& comm emit callbackMessage(result); } -void JsonProcessor::handleServerInfoCommand(const QJsonObject&, const QString& command, const int tan) +void JsonAPI::handleServerInfoCommand(const QJsonObject& message, const QString& command, const int tan) { - // create result - QJsonObject result; - result["success"] = true; - result["command"] = command; - result["tan"] = tan; - QJsonObject info; // collect priority information QJsonArray priorities; uint64_t now = QDateTime::currentMSecsSinceEpoch(); QList activePriorities = _hyperion->getActivePriorities(); - Hyperion::PriorityRegister priorityRegister = _hyperion->getPriorityRegister(); + activePriorities.removeAll(255); int currentPriority = _hyperion->getCurrentPriority(); foreach (int priority, activePriorities) { const Hyperion::InputInfo & priorityInfo = _hyperion->getPriorityInfo(priority); QJsonObject item; item["priority"] = priority; - if (priorityInfo.timeoutTime_ms != -1 ) - { + if (priorityInfo.timeoutTime_ms > 0 ) item["duration_ms"] = int(priorityInfo.timeoutTime_ms - now); - } - item["owner"] = QString(hyperion::componentToIdString(priorityInfo.componentId)); + // 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"] = true; + item["active"] = (priorityInfo.timeoutTime_ms >= -1); item["visible"] = (priority == currentPriority); - // remove item from prio register, because we have more valuable information via active priority - QList prios = priorityRegister.keys(priority); - if (! prios.empty()) - { - item["owner"] = prios[0]; - priorityRegister.remove(prios[0]); - } - - if(priorityInfo.componentId == hyperion::COMP_COLOR) + if(priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) { QJsonObject LEDcolor; @@ -413,37 +312,12 @@ void JsonProcessor::handleServerInfoCommand(const QJsonObject&, const QString& c HSLValue.append(Luminace); LEDcolor.insert("HSL", HSLValue); - // add HEX Value to Array ["HEX Value"] - QJsonArray HEXValue; - std::stringstream hex; - hex << "0x" - << std::uppercase << std::setw(2) << std::setfill('0') - << std::hex << unsigned(priorityInfo.ledColors.begin()->red) - << std::uppercase << std::setw(2) << std::setfill('0') - << std::hex << unsigned(priorityInfo.ledColors.begin()->green) - << std::uppercase << std::setw(2) << std::setfill('0') - << std::hex << unsigned(priorityInfo.ledColors.begin()->blue); - - HEXValue.append(QString::fromStdString(hex.str())); - LEDcolor.insert("HEX", HEXValue); - item["value"] = LEDcolor; } // priorities[priorities.size()] = item; priorities.append(item); } - // append left over priorities - for(auto key : priorityRegister.keys()) - { - QJsonObject item; - item["priority"] = priorityRegister[key]; - item["active"] = false; - item["visible"] = false; - item["owner"] = key; - priorities.append(item); - } - info["priorities"] = priorities; info["priorities_autoselect"] = _hyperion->sourceAutoSelectEnabled(); @@ -461,12 +335,6 @@ void JsonProcessor::handleServerInfoCommand(const QJsonObject&, const QString& c QJsonObject adjustment; adjustment["id"] = adjustmentId; - QJsonArray blackAdjust; - blackAdjust.append(colorAdjustment->_rgbBlackAdjustment.getAdjustmentR()); - blackAdjust.append(colorAdjustment->_rgbBlackAdjustment.getAdjustmentG()); - blackAdjust.append(colorAdjustment->_rgbBlackAdjustment.getAdjustmentB()); - adjustment.insert("black", blackAdjust); - QJsonArray whiteAdjust; whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentR()); whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentG()); @@ -539,9 +407,9 @@ void JsonProcessor::handleServerInfoCommand(const QJsonObject&, const QString& c // get available led devices QJsonObject ledDevices; - ledDevices["active"] = LedDevice::activeDevice(); + ledDevices["active"] = _hyperion->getActiveDevice(); QJsonArray availableLedDevices; - for (auto dev: LedDevice::getDeviceMap()) + for (auto dev: LedDeviceWrapper::getDeviceMap()) { availableLedDevices.append(dev.first); } @@ -560,7 +428,7 @@ void JsonProcessor::handleServerInfoCommand(const QJsonObject&, const QString& c } #endif grabbers["available"] = availableGrabbers; - grabbers["videomode"] = QString(videoMode2String(_hyperion->getCurrentVideoMode())); + info["videomode"] = QString(videoMode2String(_hyperion->getCurrentVideoMode())); info["grabbers"] = grabbers; // get available components @@ -576,17 +444,19 @@ void JsonProcessor::handleServerInfoCommand(const QJsonObject&, const QString& c } info["components"] = component; - info["ledMAppingType"] = ImageProcessor::mappingTypeToStr(_hyperion->getLedMappingType()); + info["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(_hyperion->getLedMappingType()); // Add Hyperion QJsonObject hyperion; hyperion["config_modified" ] = _hyperion->configModified(); hyperion["config_writeable"] = _hyperion->configWriteable(); - hyperion["off"] = hyperionIsActive()? false : true; + hyperion["enabled"] = _hyperion->getComponentRegister().isComponentEnabled(hyperion::COMP_ALL) ? true : false; - // sessions + info["hyperion"] = hyperion; + + // add sessions QJsonArray sessions; - for (auto session: _hyperion->getHyperionSessions()) + for (auto session: BonjourBrowserWrapper::getInstance()->getAllServices()) { if (session.port<0) continue; QJsonObject item; @@ -598,41 +468,156 @@ void JsonProcessor::handleServerInfoCommand(const QJsonObject&, const QString& c item["port"] = session.port; sessions.append(item); } - hyperion["sessions"] = sessions; + info["sessions"] = sessions; - info["hyperion"] = hyperion; + // BEGIN | The following entries are derecated but used to ensure backward compatibility with hyperion Classic remote control + // TODO Output the real transformation information instead of default - // send the result - result["info"] = info; - emit callbackMessage(result); + // 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["valueGain"] = 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; + + // ACTIVE EFFECT INFO + QJsonArray activeEffects; + const std::list & activeEffectsDefinitions = _hyperion->getActiveEffects(); + for (const ActiveEffectDefinition & activeEffectDefinition : activeEffectsDefinitions) + { + 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; + + // ACTIVE STATIC LED COLOR + QJsonArray activeLedColors; + const Hyperion::InputInfo & priorityInfo = _hyperion->getPriorityInfo(_hyperion->getCurrentPriority()); + if(priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) + { + QJsonObject LEDcolor; + // check if LED Color not Black (0,0,0) + if ((priorityInfo.ledColors.begin()->red + + priorityInfo.ledColors.begin()->green + + 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()) + { + subsArr.append(entry); + } + } + for(const auto & entry : subsArr) + { + if(entry == "settings-update") + continue; + + if(!_jsonCB->subscribeFor(entry.toString())) + sendErrorReply(QString("Subscription for '%1' not found. Possible values: %2").arg(entry.toString(), _jsonCB->getCommands().join(", ")), command, tan); + } + } } -void JsonProcessor::handleClearCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleClearCommand(const QJsonObject& message, const QString& command, const int tan) { emit forwardJsonMessage(message); - // extract parameters int priority = message["priority"].toInt(); - // clear priority - _hyperion->clear(priority); + if(priority > 0) + _hyperion->clear(priority); + else if(priority < 0) + _hyperion->clearall(); + else + { + sendErrorReply("Priority 0 is not allowed", command, tan); + return; + } // send reply sendSuccessReply(command, tan); } -void JsonProcessor::handleClearallCommand(const QJsonObject& message, const QString& command, const int tan) -{ - emit forwardJsonMessage(message); - - // clear priority - _hyperion->clearall(); - - // send reply - sendSuccessReply(command, tan); -} - -void JsonProcessor::handleAdjustmentCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleAdjustmentCommand(const QJsonObject& message, const QString& command, const int tan) { const QJsonObject & adjustment = message["adjustment"].toObject(); @@ -676,11 +661,6 @@ void JsonProcessor::handleAdjustmentCommand(const QJsonObject& message, const QS const QJsonArray & values = adjustment["yellow"].toArray(); colorAdjustment->_rgbYellowAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); } - if (adjustment.contains("black")) - { - const QJsonArray & values = adjustment["black"].toArray(); - colorAdjustment->_rgbBlackAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } if (adjustment.contains("white")) { const QJsonArray & values = adjustment["white"].toArray(); @@ -723,7 +703,7 @@ void JsonProcessor::handleAdjustmentCommand(const QJsonObject& message, const QS sendSuccessReply(command, tan); } -void JsonProcessor::handleSourceSelectCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleSourceSelectCommand(const QJsonObject& message, const QString& command, const int tan) { bool success = false; if (message["auto"].toBool(false)) @@ -746,7 +726,7 @@ void JsonProcessor::handleSourceSelectCommand(const QJsonObject& message, const } } -void JsonProcessor::handleConfigCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleConfigCommand(const QJsonObject& message, const QString& command, const int tan) { QString subcommand = message["subcommand"].toString(""); QString full_command = command + "-" + subcommand; @@ -761,7 +741,7 @@ void JsonProcessor::handleConfigCommand(const QJsonObject& message, const QStrin } else if (subcommand == "getconfig") { - handleConfigGetCommand(message, full_command, tan); + sendSuccessDataReply(QJsonDocument(_hyperion->getQJsonConfig()), full_command, tan); } else if (subcommand == "reload") { @@ -775,88 +755,27 @@ void JsonProcessor::handleConfigCommand(const QJsonObject& message, const QStrin } } -void JsonProcessor::handleConfigSetCommand(const QJsonObject& message, const QString &command, const int tan) +void JsonAPI::handleConfigSetCommand(const QJsonObject& message, const QString &command, const int tan) { - if(message.size() > 0) + if (message.contains("config")) { - if (message.contains("config")) + QJsonObject config = message["config"].toObject(); + if(_hyperion->getComponentRegister().isComponentEnabled(hyperion::COMP_ALL)) { - QJsonObject hyperionConfigJsonObj = message["config"].toObject(); - try - { - Q_INIT_RESOURCE(resource); - - QJsonObject schemaJson = QJsonFactory::readSchema(":/hyperion-schema"); - - QJsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schemaJson); - - QPair validate = schemaChecker.validate(hyperionConfigJsonObj); - - if (validate.first && validate.second) - { - QJsonFactory::writeJson(_hyperion->getConfigFileName(), hyperionConfigJsonObj); - } - else if (!validate.first && validate.second) - { - Warning(_log,"Errors have been found in the configuration file. Automatic correction is applied"); - - QStringList schemaErrors = schemaChecker.getMessages(); - for (auto & schemaError : schemaErrors) - Info(_log, QSTRING_CSTR(schemaError)); - - hyperionConfigJsonObj = schemaChecker.getAutoCorrectedConfig(hyperionConfigJsonObj); - - if (!QJsonFactory::writeJson(_hyperion->getConfigFileName(), hyperionConfigJsonObj)) - throw std::runtime_error("ERROR: can not save configuration file, aborting"); - } - else //Error in Schema - { - QString errorMsg = "ERROR: Json validation failed: \n"; - QStringList schemaErrors = schemaChecker.getMessages(); - for (auto & schemaError: schemaErrors) - { - Error(_log, "config write validation: %s", QSTRING_CSTR(schemaError)); - errorMsg += schemaError + "\n"; - } - - throw std::runtime_error(errorMsg.toStdString()); - } - sendSuccessReply(command, tan); - } - catch(const std::runtime_error& validate_error) - { - sendErrorReply("Error while validating json: " + QString(validate_error.what()), command, tan); - } + if(_hyperion->saveSettings(config, true)) + sendSuccessReply(command,tan); + else + sendErrorReply("Failed to save configuration, more information at the Hyperion log", command, tan); } - } - else - { - sendErrorReply("Error while parsing json: Message size " + QString(message.size()), command, tan); + else + sendErrorReply("Saving configuration while Hyperion is disabled isn't possible", command, tan); } } -void JsonProcessor::handleConfigGetCommand(const QJsonObject& message, const QString& command, const int tan) +void JsonAPI::handleSchemaGetCommand(const QJsonObject& message, const QString& command, const int tan) { // create result - QJsonObject result; - result["success"] = true; - result["command"] = command; - result["tan"] = tan; - - result["result"] = _hyperion->getQJsonConfig(); - - // send the result - emit callbackMessage(result); -} - -void JsonProcessor::handleSchemaGetCommand(const QJsonObject& message, const QString& command, const int tan) -{ - // create result - QJsonObject result, schemaJson, alldevices, properties; - result["success"] = true; - result["command"] = command; - result["tan"] = tan; + QJsonObject schemaJson, alldevices, properties; // make sure the resources are loaded (they may be left out after static linking) Q_INIT_RESOURCE(resource); @@ -875,7 +794,7 @@ void JsonProcessor::handleSchemaGetCommand(const QJsonObject& message, const QSt // collect all LED Devices properties = schemaJson["properties"].toObject(); - alldevices = LedDevice::getLedDeviceSchemas(); + alldevices = LedDeviceWrapper::getLedDeviceSchemas(); properties.insert("alldevices", alldevices); // collect all available effect schemas @@ -912,65 +831,39 @@ void JsonProcessor::handleSchemaGetCommand(const QJsonObject& message, const QSt schemaJson.insert("properties", properties); - result["result"] = schemaJson; - // send the result - emit callbackMessage(result); + sendSuccessDataReply(QJsonDocument(schemaJson), command, tan); } -void JsonProcessor::handleComponentStateCommand(const QJsonObject& message, const QString &command, const int tan) +void JsonAPI::handleComponentStateCommand(const QJsonObject& message, const QString &command, const int tan) { const QJsonObject & componentState = message["componentstate"].toObject(); QString compStr = componentState["component"].toString("invalid"); bool compState = componentState["state"].toBool(true); + Components component = stringToComponent(compStr); + if (compStr == "ALL" ) { - if (hyperionIsActive() != compState) - { - std::map components = _hyperion->getComponentRegister().getRegister(); + if(_hyperion->getComponentRegister().setHyperionEnable(compState)) + sendSuccessReply(command, tan); + else + sendErrorReply(QString("Hyperion is already %1").arg(compState ? "enabled" : "disabled"), command, tan ); - if (!compState) - { - JsonProcessor::_componentsPrevState = components; - } - - for(auto comp : components) - { - _hyperion->setComponentState(comp.first, compState ? JsonProcessor::_componentsPrevState[comp.first] : false); - } - - if (compState) - { - JsonProcessor::_componentsPrevState.clear(); - } - } - - sendSuccessReply(command, tan); return; - } - else + else if (component != COMP_INVALID) { - Components component = stringToComponent(compStr); - - if (hyperionIsActive()) - { - if (component != COMP_INVALID) - { - _hyperion->setComponentState(component, compState); - sendSuccessReply(command, tan); - return; - } - sendErrorReply("invalid component name", command, tan); - return; - } - sendErrorReply("can't change component state when hyperion is off", command, tan); + // send result before apply + sendSuccessReply(command, tan); + _hyperion->setComponentState(component, compState); + return; } + sendErrorReply("invalid component name", command, tan); } -void JsonProcessor::handleLedColorsCommand(const QJsonObject& message, const QString &command, const int tan) +void JsonAPI::handleLedColorsCommand(const QJsonObject& message, const QString &command, const int tan) { // create result QString subcommand = message["subcommand"].toString(""); @@ -979,23 +872,23 @@ void JsonProcessor::handleLedColorsCommand(const QJsonObject& message, const QSt { _streaming_leds_reply["success"] = true; _streaming_leds_reply["command"] = command+"-ledstream-update"; - _streaming_leds_reply["tan"] = tan; - _timer_ledcolors.start(125); + _streaming_leds_reply["tan"] = tan; + connect(_hyperion, &Hyperion::rawLedColors, this, &JsonAPI::streamLedcolorsUpdate, Qt::UniqueConnection); } else if (subcommand == "ledstream-stop") { - _timer_ledcolors.stop(); + disconnect(_hyperion, &Hyperion::rawLedColors, this, &JsonAPI::streamLedcolorsUpdate); } else if (subcommand == "imagestream-start") { _streaming_image_reply["success"] = true; _streaming_image_reply["command"] = command+"-imagestream-update"; - _streaming_image_reply["tan"] = tan; - connect(_hyperion, SIGNAL(emitImage(int, const Image&, const int)), this, SLOT(setImage(int, const Image&, const int)) ); + _streaming_image_reply["tan"] = tan; + connect(_hyperion, &Hyperion::currentImage, this, &JsonAPI::setImage, Qt::UniqueConnection); } else if (subcommand == "imagestream-stop") { - disconnect(_hyperion, SIGNAL(emitImage(int, const Image&, const int)), this, 0 ); + disconnect(_hyperion, &Hyperion::currentImage, this, &JsonAPI::setImage); } else { @@ -1006,7 +899,7 @@ void JsonProcessor::handleLedColorsCommand(const QJsonObject& message, const QSt sendSuccessReply(command+"-"+subcommand,tan); } -void JsonProcessor::handleLoggingCommand(const QJsonObject& message, const QString &command, const int tan) +void JsonAPI::handleLoggingCommand(const QJsonObject& message, const QString &command, const int tan) { // create result QString subcommand = message["subcommand"].toString(""); @@ -1042,26 +935,37 @@ void JsonProcessor::handleLoggingCommand(const QJsonObject& message, const QStri sendSuccessReply(command+"-"+subcommand,tan); } -void JsonProcessor::handleProcessingCommand(const QJsonObject& message, const QString &command, const int tan) +void JsonAPI::handleProcessingCommand(const QJsonObject& message, const QString &command, const int tan) { _hyperion->setLedMappingType(ImageProcessor::mappingTypeToInt( message["mappingType"].toString("multicolor_mean")) ); sendSuccessReply(command, tan); } -void JsonProcessor::handleVideoModeCommand(const QJsonObject& message, const QString &command, const int tan) +void JsonAPI::handleVideoModeCommand(const QJsonObject& message, const QString &command, const int tan) { _hyperion->setVideoMode(parse3DMode(message["videoMode"].toString("2D"))); sendSuccessReply(command, tan); } -void JsonProcessor::handleNotImplemented() +void JsonAPI::handleClearallCommand(const QJsonObject& message, const QString& command, const int tan) +{ + emit forwardJsonMessage(message); + + // clear priority + _hyperion->clearall(); + + // send reply + sendSuccessReply(command, tan); +} + +void JsonAPI::handleNotImplemented() { sendErrorReply("Command not implemented"); } -void JsonProcessor::sendSuccessReply(const QString &command, const int tan) +void JsonAPI::sendSuccessReply(const QString &command, const int tan) { // create reply QJsonObject reply; @@ -1071,17 +975,23 @@ void JsonProcessor::sendSuccessReply(const QString &command, const int tan) // send reply emit callbackMessage(reply); - - // blacklisted commands for emitter - QVector vector; - vector << "ledcolors-imagestream-stop" << "ledcolors-imagestream-start" << "ledcolors-ledstream-stop" << "ledcolors-ledstream-start" << "logging-start" << "logging-stop"; - if(vector.indexOf(command) == -1) - { - emit pushReq(); - } } -void JsonProcessor::sendErrorReply(const QString &error, const QString &command, const int tan) +void JsonAPI::sendSuccessDataReply(const QJsonDocument &doc, const QString &command, const int &tan) +{ + QJsonObject reply; + reply["success"] = true; + reply["command"] = command; + reply["tan"] = tan; + if(doc.isArray()) + reply["info"] = doc.array(); + else + reply["info"] = doc.object(); + + emit callbackMessage(reply); +} + +void JsonAPI::sendErrorReply(const QString &error, const QString &command, const int tan) { // create reply QJsonObject reply; @@ -1095,32 +1005,37 @@ void JsonProcessor::sendErrorReply(const QString &error, const QString &command, } -void JsonProcessor::streamLedcolorsUpdate() +void JsonAPI::streamLedcolorsUpdate(const std::vector& ledColors) { - QJsonObject result; - QJsonArray leds; - - const PriorityMuxer::InputInfo & priorityInfo = _hyperion->getPriorityInfo(_hyperion->getCurrentPriority()); - for(auto color = priorityInfo.ledColors.begin(); color != priorityInfo.ledColors.end(); ++color) + QMutexLocker lock(&_led_stream_mutex); + if ( (_led_stream_timeout+100) < QDateTime::currentMSecsSinceEpoch() ) { - QJsonObject item; - item["index"] = int(color - priorityInfo.ledColors.begin()); - item["red"] = color->red; - item["green"] = color->green; - item["blue"] = color->blue; - leds.append(item); + _led_stream_timeout = QDateTime::currentMSecsSinceEpoch(); + QJsonObject result; + QJsonArray leds; + + for(auto color = ledColors.begin(); color != ledColors.end(); ++color) + { + QJsonObject item; + item["index"] = int(color - ledColors.begin()); + item["red"] = color->red; + item["green"] = color->green; + item["blue"] = color->blue; + leds.append(item); + } + + result["leds"] = leds; + _streaming_leds_reply["result"] = result; + + // send the result + emit callbackMessage(_streaming_leds_reply); } - - result["leds"] = leds; - _streaming_leds_reply["result"] = result; - - // send the result - emit callbackMessage(_streaming_leds_reply); } -void JsonProcessor::setImage(int priority, const Image & image, int duration_ms) +void JsonAPI::setImage(const Image & image) { - if ( (_image_stream_timeout+250) < QDateTime::currentMSecsSinceEpoch() && _image_stream_mutex.tryLock(0) ) + QMutexLocker lock(&_image_stream_mutex); + if ( (_image_stream_timeout+100) < QDateTime::currentMSecsSinceEpoch() ) { _image_stream_timeout = QDateTime::currentMSecsSinceEpoch(); @@ -1134,12 +1049,10 @@ void JsonProcessor::setImage(int priority, const Image & image, int du result["image"] = "data:image/jpg;base64,"+QString(ba.toBase64()); _streaming_image_reply["result"] = result; emit callbackMessage(_streaming_image_reply); - - _image_stream_mutex.unlock(); } } -void JsonProcessor::incommingLogMessage(Logger::T_LOG_MESSAGE msg) +void JsonAPI::incommingLogMessage(Logger::T_LOG_MESSAGE msg) { QJsonObject result, message; QJsonArray messageArray; @@ -1180,11 +1093,3 @@ void JsonProcessor::incommingLogMessage(Logger::T_LOG_MESSAGE msg) // send the result emit callbackMessage(_streaming_logging_reply); } - -void JsonProcessor::forceServerInfo() -{ - const QString command("serverinfo"); - const int tan = 1; - const QJsonObject obj; - handleServerInfoCommand(obj,command,tan); -} diff --git a/libsrc/api/JsonCB.cpp b/libsrc/api/JsonCB.cpp new file mode 100644 index 00000000..1fe0be1c --- /dev/null +++ b/libsrc/api/JsonCB.cpp @@ -0,0 +1,303 @@ +// proj incl +#include + +// hyperion +#include +// components +#include +// bonjour wrapper +#include +// priorityMuxer +#include +#include +#include + +// Image to led map helper +#include + +using namespace hyperion; + +JsonCB::JsonCB(QObject* parent) + : QObject(parent) + , _hyperion(Hyperion::getInstance()) + , _componentRegister(& _hyperion->getComponentRegister()) + , _bonjour(BonjourBrowserWrapper::getInstance()) + , _prioMuxer(_hyperion->getMuxerInstance()) +{ + _availableCommands << "components-update" << "sessions-update" << "priorities-update" << "imageToLedMapping-update" + << "adjustment-update" << "videomode-update" << "effects-update" << "settings-update"; +} + +bool JsonCB::subscribeFor(const QString& type) +{ + if(!_availableCommands.contains(type)) + return false; + + if(type == "components-update") + { + _subscribedCommands << type; + connect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCB::handleComponentState, Qt::UniqueConnection); + } + + if(type == "sessions-update") + { + _subscribedCommands << type; + connect(_bonjour, &BonjourBrowserWrapper::browserChange, this, &JsonCB::handleBonjourChange, Qt::UniqueConnection); + } + + if(type == "priorities-update") + { + _subscribedCommands << type; + connect(_prioMuxer, &PriorityMuxer::prioritiesChanged, this, &JsonCB::handlePriorityUpdate, Qt::UniqueConnection); + connect(_prioMuxer, &PriorityMuxer::autoSelectChanged, this, &JsonCB::handlePriorityUpdate, Qt::UniqueConnection); + } + + if(type == "imageToLedMapping-update") + { + _subscribedCommands << type; + connect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCB::handleImageToLedsMappingChange, Qt::UniqueConnection); + } + + if(type == "adjustment-update") + { + _subscribedCommands << type; + connect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCB::handleAdjustmentChange, Qt::UniqueConnection); + } + + if(type == "videomode-update") + { + _subscribedCommands << type; + connect(_hyperion, &Hyperion::newVideoMode, this, &JsonCB::handleVideoModeChange, Qt::UniqueConnection); + } + + if(type == "effects-update") + { + _subscribedCommands << type; + connect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCB::handleEffectListChange, Qt::UniqueConnection); + } + + if(type == "settings-update") + { + _subscribedCommands << type; + connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleSettingsChange, Qt::UniqueConnection); + } + + return true; +} + +void JsonCB::doCallback(const QString& cmd, const QVariant& data) +{ + QJsonObject obj; + obj["command"] = cmd; + + if(static_cast(data.type()) == QMetaType::QJsonArray) + obj["data"] = data.toJsonArray(); + else + obj["data"] = data.toJsonObject(); + + emit newCallback(obj); +} + +void JsonCB::handleComponentState(const hyperion::Components comp, const bool state) +{ + QJsonObject data; + data["name"] = componentToIdString(comp); + data["enabled"] = state; + + doCallback("components-update", QVariant(data)); +} + +void JsonCB::handleBonjourChange(const QMap& bRegisters) +{ + QJsonArray data; + for (const auto & session: bRegisters) + { + if (session.port<0) continue; + QJsonObject item; + item["name"] = session.serviceName; + item["type"] = session.registeredType; + item["domain"] = session.replyDomain; + item["host"] = session.hostName; + item["address"]= session.address; + item["port"] = session.port; + data.append(item); + } + + doCallback("sessions-update", QVariant(data)); +} + +void JsonCB::handlePriorityUpdate() +{ + QJsonObject data; + QJsonArray priorities; + uint64_t now = QDateTime::currentMSecsSinceEpoch(); + QList activePriorities = _prioMuxer->getPriorities(); + activePriorities.removeAll(255); + int currentPriority = _prioMuxer->getCurrentPriority(); + + foreach (int priority, activePriorities) { + const Hyperion::InputInfo priorityInfo = _prioMuxer->getInputInfo(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, 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(const 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(const VideoMode& mode) +{ + QJsonObject data; + data["videomode"] = QString(videoMode2String(mode)); + doCallback("videomode-update", QVariant(data)); +} + +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)); +} + +void JsonCB::handleSettingsChange(const 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)); +} diff --git a/libsrc/blackborder/BlackBorderDetector.cpp b/libsrc/blackborder/BlackBorderDetector.cpp index 8d789e31..e1a5249b 100644 --- a/libsrc/blackborder/BlackBorderDetector.cpp +++ b/libsrc/blackborder/BlackBorderDetector.cpp @@ -23,7 +23,7 @@ uint8_t BlackBorderDetector::calculateThreshold(double threshold) uint8_t blackborderThreshold = uint8_t(rgbThreshold); - Debug(Logger::getInstance("BLACKBORDER"), "threshold set to %f (%d)", threshold , int(blackborderThreshold)); + //Debug(Logger::getInstance("BLACKBORDER"), "threshold set to %f (%d)", threshold , int(blackborderThreshold)); return blackborderThreshold; } diff --git a/libsrc/blackborder/BlackBorderProcessor.cpp b/libsrc/blackborder/BlackBorderProcessor.cpp index c1c12fef..a566df17 100644 --- a/libsrc/blackborder/BlackBorderProcessor.cpp +++ b/libsrc/blackborder/BlackBorderProcessor.cpp @@ -1,32 +1,109 @@ #include -#include +#include // Blackborder includes #include - using namespace hyperion; -BlackBorderProcessor::BlackBorderProcessor(const QJsonObject &blackborderConfig) - : _enabled(blackborderConfig["enable"].toBool(true)) - , _unknownSwitchCnt(blackborderConfig["unknownFrameCnt"].toInt(600)) - , _borderSwitchCnt(blackborderConfig["borderFrameCnt"].toInt(50)) - , _maxInconsistentCnt(blackborderConfig["maxInconsistentCnt"].toInt(10)) - , _blurRemoveCnt(blackborderConfig["blurRemoveCnt"].toInt(1)) - , _detectionMode(blackborderConfig["mode"].toString("default")) - , _detector(blackborderConfig["threshold"].toDouble(5.0)/100) +BlackBorderProcessor::BlackBorderProcessor(Hyperion* hyperion, QObject* parent) + : QObject(parent) + , _hyperion(hyperion) + , _enabled(false) + , _unknownSwitchCnt(600) + , _borderSwitchCnt(50) + , _maxInconsistentCnt(10) + , _blurRemoveCnt(1) + , _detectionMode("default") + , _detector(nullptr) , _currentBorder({true, -1, -1}) , _previousDetectedBorder({true, -1, -1}) , _consistentCnt(0) , _inconsistentCnt(10) + , _oldThreshold(-0.1) + , _hardDisabled(false) + , _userEnabled(false) { - if (_enabled) + // init + handleSettingsUpdate(settings::BLACKBORDER, _hyperion->getSetting(settings::BLACKBORDER)); + + // listen for settings updates + connect(_hyperion, &Hyperion::settingsChanged, this, &BlackBorderProcessor::handleSettingsUpdate); + + // listen for component state changes + connect(_hyperion, &Hyperion::componentStateChanged, this, &BlackBorderProcessor::componentStateChanged); +} + +BlackBorderProcessor::~BlackBorderProcessor() +{ + delete _detector; +} + +void BlackBorderProcessor::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::BLACKBORDER) { - Debug(Logger::getInstance("BLACKBORDER"), "mode: %s", QSTRING_CSTR(_detectionMode)); + const QJsonObject& obj = config.object(); + _unknownSwitchCnt = obj["unknownFrameCnt"].toInt(600); + _borderSwitchCnt = obj["borderFrameCnt"].toInt(50); + _maxInconsistentCnt = obj["maxInconsistentCnt"].toInt(10); + _blurRemoveCnt = obj["blurRemoveCnt"].toInt(1); + _detectionMode = obj["mode"].toString("default"); + const double newThreshold = obj["threshold"].toDouble(5.0)/100.0; + + if(_oldThreshold != newThreshold) + { + _oldThreshold = newThreshold; + + if(_detector != nullptr) + delete _detector; + + _detector = new BlackBorderDetector(newThreshold); + } + + Debug(Logger::getInstance("BLACKBORDER"), "Set mode to: %s", QSTRING_CSTR(_detectionMode)); + + // eval the comp state + componentStateChanged(hyperion::COMP_BLACKBORDER, obj["enable"].toBool(true)); } } +void BlackBorderProcessor::componentStateChanged(const hyperion::Components component, bool enable) +{ + if(component == hyperion::COMP_BLACKBORDER) + { + _userEnabled = enable; + if(enable) + { + // eg effects and probably other components don't want a BB, mimik a wrong comp state to the comp register + if(!_hardDisabled) + _enabled = enable; + } + else + { + _enabled = enable; + } + + _hyperion->getComponentRegister().componentStateChanged(hyperion::COMP_BLACKBORDER, enable); + } +} + +void BlackBorderProcessor::setHardDisable(const bool& disable) { + + if (disable) + { + _enabled = false; + } + else + { + // the user has the last word to enable + if(_userEnabled) + _enabled = true; + } + _hardDisabled = disable; +}; + BlackBorder BlackBorderProcessor::getCurrentBorder() const { return _currentBorder; diff --git a/libsrc/blackborder/CMakeLists.txt b/libsrc/blackborder/CMakeLists.txt index 023ebb60..e93c5e47 100644 --- a/libsrc/blackborder/CMakeLists.txt +++ b/libsrc/blackborder/CMakeLists.txt @@ -7,4 +7,7 @@ FILE ( GLOB Blackborder_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_D add_library(blackborder ${Blackborder_SOURCES} ) -target_link_libraries(blackborder hyperion-utils ) +target_link_libraries(blackborder + hyperion-utils + hyperion +) diff --git a/libsrc/boblightserver/BoblightClientConnection.cpp b/libsrc/boblightserver/BoblightClientConnection.cpp index 17dc3491..92828df2 100644 --- a/libsrc/boblightserver/BoblightClientConnection.cpp +++ b/libsrc/boblightserver/BoblightClientConnection.cpp @@ -15,23 +15,22 @@ #include // hyperion util includes -#include "hyperion/ImageProcessorFactory.h" -#include "hyperion/ImageProcessor.h" -#include "utils/ColorRgb.h" +#include #include "HyperionConfig.h" +#include // project includes #include "BoblightClientConnection.h" -BoblightClientConnection::BoblightClientConnection(QTcpSocket *socket, const int priority) +BoblightClientConnection::BoblightClientConnection(Hyperion* hyperion, QTcpSocket *socket, const int priority) : QObject() , _locale(QLocale::C) , _socket(socket) - , _imageProcessor(ImageProcessorFactory::getInstance().newImageProcessor()) - , _hyperion(Hyperion::getInstance()) + , _imageProcessor(hyperion->getImageProcessor()) + , _hyperion(hyperion) , _receiveBuffer() , _priority(priority) - , _ledColors(Hyperion::getInstance()->getLedCount(), ColorRgb::BLACK) + , _ledColors(hyperion->getLedCount(), ColorRgb::BLACK) , _log(Logger::getInstance("BOBLIGHT")) , _clientAddress(QHostInfo::fromName(socket->peerAddress().toString()).hostName()) { @@ -167,7 +166,7 @@ void BoblightClientConnection::handleMessage(const QString & message) // send current color values to hyperion if this is the last led assuming leds values are send in order of id if ((ledIndex == _ledColors.size() -1) && _priority < 255) { - _hyperion->setColors(_priority, _ledColors, -1, true, hyperion::COMP_BOBLIGHTSERVER, _clientAddress); + _hyperion->setInput(_priority, _ledColors); } return; @@ -205,7 +204,7 @@ void BoblightClientConnection::handleMessage(const QString & message) // send current color values to hyperion if (_priority < 255) { - _hyperion->setColors(_priority, _ledColors, -1, true, hyperion::COMP_BOBLIGHTSERVER, _clientAddress); + _hyperion->setInput(_priority, _ledColors); } return; } diff --git a/libsrc/boblightserver/BoblightClientConnection.h b/libsrc/boblightserver/BoblightClientConnection.h index d4baffa6..f4e4d00e 100644 --- a/libsrc/boblightserver/BoblightClientConnection.h +++ b/libsrc/boblightserver/BoblightClientConnection.h @@ -5,11 +5,12 @@ #include #include -// Hyperion includes -#include +// utils includes #include +#include class ImageProcessor; +class Hyperion; /// /// The Connection object created by \a BoblightServer when a new connection is establshed @@ -24,7 +25,7 @@ public: /// @param socket The Socket object for this connection /// @param hyperion The Hyperion server /// - BoblightClientConnection(QTcpSocket * socket, const int priority); + BoblightClientConnection(Hyperion* hyperion, QTcpSocket * socket, const int priority); /// /// Destructor @@ -90,7 +91,7 @@ private: /// The latest led color data std::vector _ledColors; - + /// logger instance Logger * _log; diff --git a/libsrc/boblightserver/BoblightServer.cpp b/libsrc/boblightserver/BoblightServer.cpp index 68a45f0c..21e0da35 100644 --- a/libsrc/boblightserver/BoblightServer.cpp +++ b/libsrc/boblightserver/BoblightServer.cpp @@ -5,20 +5,31 @@ #include #include "BoblightClientConnection.h" +// hyperion includes +#include +// qt incl +#include + using namespace hyperion; -BoblightServer::BoblightServer(const int priority, uint16_t port) +BoblightServer::BoblightServer(Hyperion* hyperion,const QJsonDocument& config) : QObject() - , _hyperion(Hyperion::getInstance()) - , _server() + , _hyperion(hyperion) + , _server(new QTcpServer(this)) , _openConnections() - , _priority(priority) + , _priority(0) , _log(Logger::getInstance("BOBLIGHT")) - , _isActive(false) - , _port(port) + , _port(0) { - // Set trigger for incoming connections - connect(&_server, SIGNAL(newConnection()), this, SLOT(newConnection())); + Debug(_log, "Instance created"); + + // listen for component change + connect(_hyperion, SIGNAL(componentStateChanged(hyperion::Components,bool)), this, SLOT(componentStateChanged(hyperion::Components,bool))); + // listen new connection signal from server + connect(_server, SIGNAL(newConnection()), this, SLOT(newConnection())); + + // init + handleSettingsUpdate(settings::BOBLSERVER, config); } BoblightServer::~BoblightServer() @@ -28,64 +39,64 @@ BoblightServer::~BoblightServer() void BoblightServer::start() { - if ( active() ) + if ( _server->isListening() ) return; - - if (!_server.listen(QHostAddress::Any, _port)) + + if (!_server->listen(QHostAddress::Any, _port)) { - throw std::runtime_error("BOBLIGHT ERROR: server could not bind to port"); + Error(_log, "Could not bind to port '%d', please use an available port", _port); + return; } - Info(_log, "Boblight server started on port %d", _port); + Info(_log, "Started on port %d", _port); - _isActive = true; - emit statusChanged(_isActive); - - _hyperion->registerPriority("Boblight", _priority); + _hyperion->getComponentRegister().componentStateChanged(COMP_BOBLIGHTSERVER, _server->isListening()); } void BoblightServer::stop() { - if ( ! active() ) + if ( ! _server->isListening() ) return; - + foreach (BoblightClientConnection * connection, _openConnections) { delete connection; } - _server.close(); - _isActive = false; - emit statusChanged(_isActive); + _server->close(); - _hyperion->unRegisterPriority("Boblight"); + Info(_log, "Stopped"); + _hyperion->getComponentRegister().componentStateChanged(COMP_BOBLIGHTSERVER, _server->isListening()); +} +bool BoblightServer::active() +{ + return _server->isListening(); } void BoblightServer::componentStateChanged(const hyperion::Components component, bool enable) { if (component == COMP_BOBLIGHTSERVER) { - if (_isActive != enable) + if (_server->isListening() != enable) { if (enable) start(); else stop(); - Info(_log, "change state to %s", (_isActive ? "enabled" : "disabled") ); } - _hyperion->getComponentRegister().componentStateChanged(component, _isActive); } } uint16_t BoblightServer::getPort() const { - return _server.serverPort(); + return _server->serverPort(); } void BoblightServer::newConnection() { - QTcpSocket * socket = _server.nextPendingConnection(); + QTcpSocket * socket = _server->nextPendingConnection(); if (socket != nullptr) { Info(_log, "new connection"); - BoblightClientConnection * connection = new BoblightClientConnection(socket, _priority); + _hyperion->registerInput(_priority, hyperion::COMP_BOBLIGHTSERVER, QString("Boblight@%1").arg(socket->peerAddress().toString())); + BoblightClientConnection * connection = new BoblightClientConnection(_hyperion, socket, _priority); _openConnections.insert(connection); // register slot for cleaning up after the connection closed @@ -101,3 +112,16 @@ void BoblightServer::closedConnection(BoblightClientConnection *connection) // schedule to delete the connection object connection->deleteLater(); } + +void BoblightServer::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::BOBLSERVER) + { + QJsonObject obj = config.object(); + _port = obj["port"].toInt(); + _priority = obj["priority"].toInt(); + stop(); + if(obj["enable"].toBool()) + start(); + } +} diff --git a/libsrc/bonjour/bonjourbrowserwrapper.cpp b/libsrc/bonjour/bonjourbrowserwrapper.cpp new file mode 100644 index 00000000..8b8c8a67 --- /dev/null +++ b/libsrc/bonjour/bonjourbrowserwrapper.cpp @@ -0,0 +1,84 @@ +#include + +//qt incl +#include + +// bonjour +#include +#include + +BonjourBrowserWrapper* BonjourBrowserWrapper::instance = nullptr; + +BonjourBrowserWrapper::BonjourBrowserWrapper(QObject * parent) + : QObject(parent) + , _bonjourResolver(new BonjourServiceResolver(this)) + , _timerBonjourResolver( new QTimer(this)) +{ + // register meta + qRegisterMetaType>("QMap"); + + BonjourBrowserWrapper::instance = this; + connect(_bonjourResolver, &BonjourServiceResolver::bonjourRecordResolved, this, &BonjourBrowserWrapper::bonjourRecordResolved); + + connect(_timerBonjourResolver, &QTimer::timeout, this, &BonjourBrowserWrapper::bonjourResolve); + _timerBonjourResolver->setInterval(1000); + _timerBonjourResolver->start(); + + // browse for _hyperiond-http._tcp + browseForServiceType(QLatin1String("_hyperiond-http._tcp")); +} + +bool BonjourBrowserWrapper::browseForServiceType(const QString &serviceType) +{ + if(!_browsedServices.contains(serviceType)) + { + BonjourServiceBrowser* newBrowser = new BonjourServiceBrowser(this); + connect(newBrowser, &BonjourServiceBrowser::currentBonjourRecordsChanged, this, &BonjourBrowserWrapper::currentBonjourRecordsChanged); + newBrowser->browseForServiceType(serviceType); + _browsedServices.insert(serviceType, newBrowser); + return true; + } + return false; +} + +void BonjourBrowserWrapper::currentBonjourRecordsChanged(const QList &list) +{ + _hyperionSessions.clear(); + for ( auto rec : list ) + { + _hyperionSessions.insert(rec.serviceName, rec); + } +} + +void BonjourBrowserWrapper::bonjourRecordResolved(const QHostInfo &hostInfo, int port) +{ + if ( _hyperionSessions.contains(_bonjourCurrentServiceToResolve)) + { + QString host = hostInfo.hostName(); + QString domain = _hyperionSessions[_bonjourCurrentServiceToResolve].replyDomain; + if (host.endsWith("."+domain)) + { + host.remove(host.length()-domain.length()-1,domain.length()+1); + } + _hyperionSessions[_bonjourCurrentServiceToResolve].hostName = host; + _hyperionSessions[_bonjourCurrentServiceToResolve].port = port; + _hyperionSessions[_bonjourCurrentServiceToResolve].address = hostInfo.addresses().isEmpty() ? "" : hostInfo.addresses().first().toString(); + //Debug(_log, "found hyperion session: %s:%d",QSTRING_CSTR(hostInfo.hostName()), port); + + //emit change + emit browserChange(_hyperionSessions); + } +} + +void BonjourBrowserWrapper::bonjourResolve() +{ + for(auto key : _hyperionSessions.keys()) + { + if (_hyperionSessions[key].port < 0) + { + _bonjourCurrentServiceToResolve = key; + _bonjourResolver->resolveBonjourRecord(_hyperionSessions[key]); + break; + } + } +} diff --git a/libsrc/bonjour/bonjourserviceregister.cpp b/libsrc/bonjour/bonjourserviceregister.cpp index 96c5dc7f..d0a4609f 100755 --- a/libsrc/bonjour/bonjourserviceregister.cpp +++ b/libsrc/bonjour/bonjourserviceregister.cpp @@ -30,8 +30,11 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include #include +#include +#include BonjourServiceRegister::BonjourServiceRegister(QObject *parent) : QObject(parent), dnssref(0), bonjourSocket(0) @@ -48,6 +51,20 @@ BonjourServiceRegister::~BonjourServiceRegister() } } +void BonjourServiceRegister::registerService(const QString& service, const int& port) +{ + _port = port; + // zeroconf $configname@$hostname:port + // TODO add name of the main instance + registerService( + BonjourRecord(QHostInfo::localHostName()+ ":" + QString::number(port), + service, + QString() + ), + port + ); +} + void BonjourServiceRegister::registerService(const BonjourRecord &record, quint16 servicePort, std::vector> txt) { if (dnssref) @@ -61,22 +78,25 @@ void BonjourServiceRegister::registerService(const BonjourRecord &record, quint1 bigEndianPort = 0 | ((servicePort & 0x00ff) << 8) | ((servicePort & 0xff00) >> 8); } #endif - + // base txtRec + std::vector > txtBase = {{"id",Hyperion::getInstance()->getId().toStdString()},{"version",HYPERION_VERSION}}; // create txt record TXTRecordRef txtRec; TXTRecordCreate(&txtRec,0,NULL); - // add txt records - if(!txt.empty()) + if(!txt.empty()) { - for(std::vector >::const_iterator it = txt.begin(); it != txt.end(); ++it) - { - //Debug(Logger::getInstance("BonJour"), "TXTRecord: key:%s, value:%s",it->first.c_str(),it->second.c_str()); - uint8_t txtLen = (uint8_t)strlen(it->second.c_str()); - TXTRecordSetValue(&txtRec, it->first.c_str(), txtLen, it->second.c_str()); - } + txtBase.insert(txtBase.end(), txt.begin(), txt.end()); + } + // add txt records + for(std::vector >::const_iterator it = txtBase.begin(); it != txtBase.end(); ++it) + { + //Debug(Logger::getInstance("BonJour"), "TXTRecord: key:%s, value:%s",it->first.c_str(),it->second.c_str()); + uint8_t txtLen = (uint8_t)strlen(it->second.c_str()); + TXTRecordSetValue(&txtRec, it->first.c_str(), txtLen, it->second.c_str()); } + DNSServiceErrorType err = DNSServiceRegister(&dnssref, 0, 0, record.serviceName.toUtf8().constData(), record.registeredType.toUtf8().constData(), (record.replyDomain.isEmpty() ? 0 : record.replyDomain.toUtf8().constData()), diff --git a/libsrc/bonjour/bonjourserviceresolver.cpp b/libsrc/bonjour/bonjourserviceresolver.cpp index 217ae00f..aa1a932f 100644 --- a/libsrc/bonjour/bonjourserviceresolver.cpp +++ b/libsrc/bonjour/bonjourserviceresolver.cpp @@ -117,6 +117,6 @@ void BonjourServiceResolver::bonjourResolveReply(DNSServiceRef sdRef, DNSService void BonjourServiceResolver::finishConnect(const QHostInfo &hostInfo) { - emit bonjourRecordResolved(hostInfo, bonjourPort); + emit bonjourRecordResolved(hostInfo, bonjourPort); QMetaObject::invokeMethod(this, "cleanupResolve", Qt::QueuedConnection); } diff --git a/libsrc/effectengine/CMakeLists.txt b/libsrc/effectengine/CMakeLists.txt index c016b4cc..5fb38e1a 100644 --- a/libsrc/effectengine/CMakeLists.txt +++ b/libsrc/effectengine/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(PythonLibs 3.4 REQUIRED) +find_package(PythonLibs 3.5 REQUIRED) # 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. @@ -27,6 +27,7 @@ add_library(effectengine target_link_libraries(effectengine hyperion + python Qt5::Core Qt5::Gui ${PYTHON_LIBRARIES} diff --git a/libsrc/effectengine/Effect.cpp b/libsrc/effectengine/Effect.cpp index fda2f934..8e49a76b 100644 --- a/libsrc/effectengine/Effect.cpp +++ b/libsrc/effectengine/Effect.cpp @@ -15,87 +15,35 @@ #include // effect engin eincludes -#include "Effect.h" +#include +#include #include #include -// Python method table -PyMethodDef Effect::effectMethods[] = { - {"setColor" , Effect::wrapSetColor , METH_VARARGS, "Set a new color for the leds."}, - {"setImage" , Effect::wrapSetImage , METH_VARARGS, "Set a new image to process and determine new led colors."}, - {"getImage" , Effect::wrapGetImage , METH_VARARGS, "get image data from file."}, - {"abort" , Effect::wrapAbort , METH_NOARGS, "Check if the effect should abort execution."}, - {"imageShow" , Effect::wrapImageShow , METH_VARARGS, "set current effect image to hyperion core."}, - {"imageLinearGradient" , Effect::wrapImageLinearGradient , METH_VARARGS, ""}, - {"imageConicalGradient" , Effect::wrapImageConicalGradient , METH_VARARGS, ""}, - {"imageRadialGradient" , Effect::wrapImageRadialGradient , METH_VARARGS, ""}, - {"imageSolidFill" , Effect::wrapImageSolidFill , METH_VARARGS, ""}, - {"imageDrawLine" , Effect::wrapImageDrawLine , METH_VARARGS, ""}, - {"imageDrawPoint" , Effect::wrapImageDrawPoint , METH_VARARGS, ""}, - {"imageDrawRect" , Effect::wrapImageDrawRect , METH_VARARGS, ""}, - {"imageDrawPolygon" , Effect::wrapImageDrawPolygon , METH_VARARGS, ""}, - {"imageDrawPie" , Effect::wrapImageDrawPie , METH_VARARGS, ""}, - {"imageSetPixel" , Effect::wrapImageSetPixel , METH_VARARGS, "set pixel color of image"}, - {"imageGetPixel" , Effect::wrapImageGetPixel , METH_VARARGS, "get pixel color of image"}, - {"imageSave" , Effect::wrapImageSave , METH_NOARGS, "adds a new background image"}, - {"imageMinSize" , Effect::wrapImageMinSize , METH_VARARGS, "sets minimal dimension of background image"}, - {"imageWidth" , Effect::wrapImageWidth , METH_NOARGS, "gets image width"}, - {"imageHeight" , Effect::wrapImageHeight , METH_NOARGS, "gets image height"}, - {"imageCRotate" , Effect::wrapImageCRotate , METH_VARARGS, "rotate the coordinate system by given angle"}, - {"imageCOffset" , Effect::wrapImageCOffset , METH_VARARGS, "Add offset to the coordinate system"}, - {"imageCShear" , Effect::wrapImageCShear , METH_VARARGS, "Shear of coordinate system by the given horizontal/vertical axis"}, - {"imageResetT" , Effect::wrapImageResetT , METH_NOARGS, "Resets all coords modifications (rotate,offset,shear)"}, - {NULL, NULL, 0, NULL} -}; +// python utils/ global mainthread +#include +//impl +PyThreadState* mainThreadState; - -// create the hyperion module -struct PyModuleDef Effect::moduleDef = { - PyModuleDef_HEAD_INIT, - "hyperion", /* m_name */ - "Hyperion module", /* m_doc */ - -1, /* m_size */ - Effect::effectMethods, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; - -PyObject* Effect::PyInit_hyperion() -{ - return PyModule_Create(&moduleDef); -} - -void Effect::registerHyperionExtensionModule() -{ - PyImport_AppendInittab("hyperion", &PyInit_hyperion); -} - -Effect::Effect(PyThreadState* mainThreadState, int priority, int timeout, const QString & script, const QString & name, const QJsonObject & args, const QString & origin, unsigned smoothCfg) +Effect::Effect(Hyperion *hyperion, int priority, int timeout, const QString &script, const QString &name, const QJsonObject &args, const QString &imageData) : QThread() - , _mainThreadState(mainThreadState) + , _hyperion(hyperion) , _priority(priority) , _timeout(timeout) , _script(script) , _name(name) - , _smoothCfg(smoothCfg) , _args(args) + , _imageData(imageData) , _endTime(-1) - , _imageProcessor(ImageProcessorFactory::getInstance().newImageProcessor()) , _colors() - , _origin(origin) - , _imageSize(Hyperion::getInstance()->getLedGridSize()) + , _imageSize(hyperion->getLedGridSize()) , _image(_imageSize,QImage::Format_ARGB32_Premultiplied) { - _colors.resize(_imageProcessor->getLedCount()); + _colors.resize(_hyperion->getLedCount()); _colors.fill(ColorRgb::BLACK); _log = Logger::getInstance("EFFECTENGINE"); - // disable the black border detector for effects - _imageProcessor->enableBlackBorderDetector(false); - // init effect image for image based effects, size is based on led layout _image.fill(Qt::black); _painter = new QPainter(&_image); @@ -111,8 +59,11 @@ Effect::~Effect() void Effect::run() { + // we probably need to wait until mainThreadState is available + while(mainThreadState == nullptr){}; + // get global lock - PyEval_RestoreThread(_mainThreadState); + PyEval_RestoreThread(mainThreadState); // Initialize a new thread state PyThreadState* tstate = Py_NewInterpreter(); @@ -131,13 +82,13 @@ void Effect::run() PyObject_SetAttrString(module, "__effectObj", PyCapsule_New(this, nullptr, nullptr)); // add ledCount variable to the interpreter - PyObject_SetAttrString(module, "ledCount", Py_BuildValue("i", _imageProcessor->getLedCount())); + PyObject_SetAttrString(module, "ledCount", Py_BuildValue("i", _hyperion->getLedCount())); // add minimumWriteTime variable to the interpreter - PyObject_SetAttrString(module, "latchTime", Py_BuildValue("i", Hyperion::getInstance()->getLatchTime())); + PyObject_SetAttrString(module, "latchTime", Py_BuildValue("i", _hyperion->getLatchTime())); // add a args variable to the interpreter - PyObject_SetAttrString(module, "args", json2python(_args)); + PyObject_SetAttrString(module, "args", EffectModule::json2python(_args)); // decref the module Py_XDECREF(module); @@ -286,1024 +237,3 @@ void Effect::run() Py_EndInterpreter(tstate); PyEval_ReleaseLock(); } - -PyObject *Effect::json2python(const QJsonValue &jsonData) const -{ - switch (jsonData.type()) - { - case QJsonValue::Null: - return Py_BuildValue(""); - case QJsonValue::Undefined: - return Py_BuildValue(""); - case QJsonValue::Double: - { - if (std::rint(jsonData.toDouble()) != jsonData.toDouble()) - { - return Py_BuildValue("d", jsonData.toDouble()); - } - return Py_BuildValue("i", jsonData.toInt()); - } - case QJsonValue::Bool: - return Py_BuildValue("i", jsonData.toBool() ? 1 : 0); - case QJsonValue::String: - return Py_BuildValue("s", jsonData.toString().toUtf8().constData()); - case QJsonValue::Object: - { - PyObject * dict= PyDict_New(); - QJsonObject objectData = jsonData.toObject(); - for (QJsonObject::iterator i = objectData.begin(); i != objectData.end(); ++i) - { - PyObject * obj = json2python(*i); - PyDict_SetItemString(dict, i.key().toStdString().c_str(), obj); - Py_XDECREF(obj); - } - return dict; - } - case QJsonValue::Array: - { - QJsonArray arrayData = jsonData.toArray(); - PyObject * list = PyList_New(arrayData.size()); - int index = 0; - for (QJsonArray::iterator i = arrayData.begin(); i != arrayData.end(); ++i, ++index) - { - PyObject * obj = json2python(*i); - Py_INCREF(obj); - PyList_SetItem(list, index, obj); - Py_XDECREF(obj); - } - return list; - } - } - - assert(false); - return nullptr; -} - -PyObject* Effect::wrapSetColor(PyObject *self, PyObject *args) -{ - // get the effect - Effect * effect = getEffect(); - - // check if we have aborted already - if (effect->isInterruptionRequested()) - { - return Py_BuildValue(""); - } - - // determine the timeout - int timeout = effect->_timeout; - if (timeout > 0) - { - timeout = effect->_endTime - QDateTime::currentMSecsSinceEpoch(); - - // we are done if the time has passed - if (timeout <= 0) - { - return Py_BuildValue(""); - } - } - - // check the number of arguments - int argCount = PyTuple_Size(args); - if (argCount == 3) - { - // three seperate arguments for red, green, and blue - ColorRgb color; - if (PyArg_ParseTuple(args, "bbb", &color.red, &color.green, &color.blue)) - { - effect->_colors.fill(color); - effect->setColors(effect->_priority, effect->_colors.toStdVector(), timeout, false, hyperion::COMP_EFFECT, effect->_origin, effect->_smoothCfg); - return Py_BuildValue(""); - } - return nullptr; - } - else if (argCount == 1) - { - // bytearray of values - PyObject * bytearray = nullptr; - if (PyArg_ParseTuple(args, "O", &bytearray)) - { - if (PyByteArray_Check(bytearray)) - { - size_t length = PyByteArray_Size(bytearray); - if (length == 3 * effect->_imageProcessor->getLedCount()) - { - char * data = PyByteArray_AS_STRING(bytearray); - memcpy(effect->_colors.data(), data, length); - effect->setColors(effect->_priority, effect->_colors.toStdVector(), timeout, false, hyperion::COMP_EFFECT, effect->_origin, effect->_smoothCfg); - return Py_BuildValue(""); - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should be 3*ledCount"); - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Argument is not a bytearray"); - return nullptr; - } - } - else - { - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Function expect 1 or 3 arguments"); - return nullptr; - } - - // error - PyErr_SetString(PyExc_RuntimeError, "Unknown error"); - return nullptr; -} - -PyObject* Effect::wrapSetImage(PyObject *self, PyObject *args) -{ - // get the effect - Effect * effect = getEffect(); - - // check if we have aborted already - if (effect->isInterruptionRequested()) - { - return Py_BuildValue(""); - } - - // determine the timeout - int timeout = effect->_timeout; - if (timeout > 0) - { - timeout = effect->_endTime - QDateTime::currentMSecsSinceEpoch(); - - // we are done if the time has passed - if (timeout <= 0) - { - return Py_BuildValue(""); - } - } - - // bytearray of values - int width, height; - PyObject * bytearray = nullptr; - if (PyArg_ParseTuple(args, "iiO", &width, &height, &bytearray)) - { - if (PyByteArray_Check(bytearray)) - { - int length = PyByteArray_Size(bytearray); - if (length == 3 * width * height) - { - Image image(width, height); - char * data = PyByteArray_AS_STRING(bytearray); - memcpy(image.memptr(), data, length); - std::vector v = effect->_colors.toStdVector(); - effect->_imageProcessor->process(image, v); - effect->setColors(effect->_priority, v, timeout, false, hyperion::COMP_EFFECT, effect->_origin, effect->_smoothCfg); - return Py_BuildValue(""); - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should be 3*width*height"); - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Argument 3 is not a bytearray"); - return nullptr; - } - } - else - { - return nullptr; - } - - // error - PyErr_SetString(PyExc_RuntimeError, "Unknown error"); - return nullptr; -} - -PyObject* Effect::wrapGetImage(PyObject *self, PyObject *args) -{ - Q_INIT_RESOURCE(EffectEngine); - - char *source; - if(!PyArg_ParseTuple(args, "s", &source)) - { - PyErr_SetString(PyExc_TypeError, "String required"); - return NULL; - } - - QString file = QString::fromUtf8(source); - - if (file.mid(0, 1) == ":") - file = ":/effects/"+file.mid(1); - - QImageReader reader(file); - - if (reader.canRead()) - { - PyObject* result = PyList_New(reader.imageCount()); - - for (int i = 0; i < reader.imageCount(); ++i) - { - reader.jumpToImage(i); - if (reader.canRead()) - { - QImage qimage = reader.read(); - - int width = qimage.width(); - int height = qimage.height(); - - QByteArray binaryImage; - for (int i = 0; i(qimage.scanLine(i)); - for (int j = 0; j< width; ++j) - { - binaryImage.append((char) qRed(scanline[j])); - binaryImage.append((char) qGreen(scanline[j])); - binaryImage.append((char) qBlue(scanline[j])); - } - } - PyList_SET_ITEM(result, i, Py_BuildValue("{s:i,s:i,s:O}", "imageWidth", width, "imageHeight", height, "imageData", PyByteArray_FromStringAndSize(binaryImage.constData(),binaryImage.size()))); - } - else - { - PyErr_SetString(PyExc_TypeError, reader.errorString().toUtf8().constData()); - return NULL; - } - } - return result; - } - else - { - PyErr_SetString(PyExc_TypeError, reader.errorString().toUtf8().constData()); - return NULL; - } -} - -PyObject* Effect::wrapAbort(PyObject *self, PyObject *) -{ - Effect * effect = getEffect(); - - // Test if the effect has reached it end time - if (effect->_timeout > 0 && QDateTime::currentMSecsSinceEpoch() > effect->_endTime) - { - effect->requestInterruption(); - } - - return Py_BuildValue("i", effect->isInterruptionRequested() ? 1 : 0); -} - - -PyObject* Effect::wrapImageShow(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - // determine the timeout - int timeout = effect->_timeout; - if (timeout > 0) - { - timeout = effect->_endTime - QDateTime::currentMSecsSinceEpoch(); - - // we are done if the time has passed - if (timeout <= 0) - { - return Py_BuildValue(""); - } - } - - int argCount = PyTuple_Size(args); - int imgId = -1; - bool argsOk = (argCount == 0); - if (argCount == 1 && PyArg_ParseTuple(args, "i", &imgId)) - { - argsOk = true; - } - - if ( ! argsOk || (imgId>-1 && imgId >= effect->_imageStack.size())) - { - return nullptr; - } - - - QImage * qimage = (imgId<0) ? &(effect->_image) : &(effect->_imageStack[imgId]); - int width = qimage->width(); - int height = qimage->height(); - - Image image(width, height); - QByteArray binaryImage; - - for (int i = 0; i(qimage->scanLine(i)); - for (int j = 0; j< width; ++j) - { - binaryImage.append((char) qRed(scanline[j])); - binaryImage.append((char) qGreen(scanline[j])); - binaryImage.append((char) qBlue(scanline[j])); - } - } - - memcpy(image.memptr(), binaryImage.data(), binaryImage.size()); - std::vector v = effect->_colors.toStdVector(); - effect->_imageProcessor->process(image, v); - effect->setColors(effect->_priority, v, timeout, false, hyperion::COMP_EFFECT, effect->_origin, effect->_smoothCfg); - - return Py_BuildValue(""); -} - -PyObject* Effect::wrapImageLinearGradient(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - PyObject * bytearray = nullptr; - int startRX = 0; - int startRY = 0; - int startX = 0; - int startY = 0; - int endX, width = effect->_imageSize.width(); - int endY, height = effect->_imageSize.height(); - int spread = 0; - - bool argsOK = false; - - if ( argCount == 10 && PyArg_ParseTuple(args, "iiiiiiiiOi", &startRX, &startRY, &width, &height, &startX, &startY, &endX, &endY, &bytearray, &spread) ) - { - argsOK = true; - } - if ( argCount == 6 && PyArg_ParseTuple(args, "iiiiOi", &startX, &startY, &endX, &endY, &bytearray, &spread) ) - { - argsOK = true; - } - - if (argsOK) - { - if (PyByteArray_Check(bytearray)) - { - const int length = PyByteArray_Size(bytearray); - const unsigned arrayItemLength = 5; - if (length % arrayItemLength == 0) - { - QRect myQRect(startRX,startRY,width,height); - QLinearGradient gradient(QPoint(startX,startY), QPoint(endX,endY)); - char * data = PyByteArray_AS_STRING(bytearray); - - for (int idx=0; idx(spread)); - effect->_painter->fillRect(myQRect, gradient); - - return Py_BuildValue(""); - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 5"); - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "No bytearray properly defined"); - return nullptr; - } - } - return nullptr; -} - -PyObject* Effect::wrapImageConicalGradient(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - PyObject * bytearray = nullptr; - int centerX, centerY, angle; - int startX = 0; - int startY = 0; - int width = effect->_imageSize.width(); - int height = effect->_imageSize.height(); - - bool argsOK = false; - - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiiO", &startX, &startY, &width, &height, ¢erX, ¢erY, &angle, &bytearray) ) - { - argsOK = true; - } - if ( argCount == 4 && PyArg_ParseTuple(args, "iiiO", ¢erX, ¢erY, &angle, &bytearray) ) - { - argsOK = true; - } - angle = qMax(qMin(angle,360),0); - - if (argsOK) - { - if (PyByteArray_Check(bytearray)) - { - const int length = PyByteArray_Size(bytearray); - const unsigned arrayItemLength = 5; - if (length % arrayItemLength == 0) - { - QRect myQRect(startX,startY,width,height); - QConicalGradient gradient(QPoint(centerX,centerY), angle ); - char * data = PyByteArray_AS_STRING(bytearray); - - for (int idx=0; idx_painter->fillRect(myQRect, gradient); - - return Py_BuildValue(""); - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 5"); - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Argument 8 is not a bytearray"); - return nullptr; - } - } - return nullptr; -} - - -PyObject* Effect::wrapImageRadialGradient(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - PyObject * bytearray = nullptr; - int centerX, centerY, radius, focalX, focalY, focalRadius, spread; - int startX = 0; - int startY = 0; - int width = effect->_imageSize.width(); - int height = effect->_imageSize.height(); - - bool argsOK = false; - - if ( argCount == 12 && PyArg_ParseTuple(args, "iiiiiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread) ) - { - argsOK = true; - } - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &bytearray, &spread) ) - { - argsOK = true; - focalX = centerX; - focalY = centerY; - focalRadius = radius; - } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiOi", ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread) ) - { - argsOK = true; - } - if ( argCount == 5 && PyArg_ParseTuple(args, "iiiOi", ¢erX, ¢erY, &radius, &bytearray, &spread) ) - { - argsOK = true; - focalX = centerX; - focalY = centerY; - focalRadius = radius; - } - - if (argsOK) - { - if (PyByteArray_Check(bytearray)) - { - int length = PyByteArray_Size(bytearray); - if (length % 4 == 0) - { - - QRect myQRect(startX,startY,width,height); - QRadialGradient gradient(QPoint(centerX,centerY), qMax(radius,0) ); - char * data = PyByteArray_AS_STRING(bytearray); - - for (int idx=0; idx(spread)); - effect->_painter->fillRect(myQRect, gradient); - - return Py_BuildValue(""); - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 4"); - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Last argument is not a bytearray"); - return nullptr; - } - } - return nullptr; -} - -PyObject* Effect::wrapImageDrawPolygon(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - PyObject * bytearray = nullptr; - - int argCount = PyTuple_Size(args); - int r, g, b; - int a = 255; - - bool argsOK = false; - - if ( argCount == 5 && PyArg_ParseTuple(args, "Oiiii", &bytearray, &r, &g, &b, &a) ) - { - argsOK = true; - } - if ( argCount == 4 && PyArg_ParseTuple(args, "Oiii", &bytearray, &r, &g, &b) ) - { - argsOK = true; - } - - if (argsOK) - { - if (PyByteArray_Check(bytearray)) - { - int length = PyByteArray_Size(bytearray); - if (length % 2 == 0) - { - QVector points; - char * data = PyByteArray_AS_STRING(bytearray); - - for (int idx=0; idx_painter; - QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); - painter->setPen(newPen); - painter->setBrush(QBrush(QColor(r,g,b,a), Qt::SolidPattern)); - painter->drawPolygon(points); - painter->setPen(oldPen); - return Py_BuildValue(""); - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 2"); - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Argument 1 is not a bytearray"); - return nullptr; - } - } - return nullptr; -} - -PyObject* Effect::wrapImageDrawPie(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - PyObject * bytearray = nullptr; - - QString brush; - int argCount = PyTuple_Size(args); - int radius, centerX, centerY; - int startAngle = 0; - int spanAngle = 360; - int r = 0; - int g = 0; - int b = 0; - int a = 255; - - bool argsOK = false; - - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b, &a) ) - { - argsOK = true; - } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b) ) - { - argsOK = true; - } - if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiisO", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &brush, &bytearray) ) - { - argsOK = true; - } - if ( argCount == 5 && PyArg_ParseTuple(args, "iiisO", ¢erX, ¢erY, &radius, &brush, &bytearray) ) - { - argsOK = true; - } - - if (argsOK) - { - QPainter * painter = effect->_painter; - startAngle = qMax(qMin(startAngle,360),0); - spanAngle = qMax(qMin(spanAngle,360),-360); - - if( argCount == 7 || argCount == 5 ) - { - a = 0; - if (PyByteArray_Check(bytearray)) - { - int length = PyByteArray_Size(bytearray); - if (length % 5 == 0) - { - - QConicalGradient gradient(QPoint(centerX,centerY), startAngle); - - - char * data = PyByteArray_AS_STRING(bytearray); - - for (int idx=0; idxsetBrush(gradient); - - return Py_BuildValue(""); - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 5"); - return nullptr; - } - } - else - { - PyErr_SetString(PyExc_RuntimeError, "Last argument is not a bytearray"); - return nullptr; - } - } - else - { - painter->setBrush(QBrush(QColor(r,g,b,a), Qt::SolidPattern)); - } - QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); - painter->setPen(newPen); - painter->drawPie(centerX - radius, centerY - radius, centerX + radius, centerY + radius, startAngle * 16, spanAngle * 16); - painter->setPen(oldPen); - return Py_BuildValue(""); - } - return nullptr; -} - -PyObject* Effect::wrapImageSolidFill(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int r, g, b; - int a = 255; - int startX = 0; - int startY = 0; - int width = effect->_imageSize.width(); - int height = effect->_imageSize.height(); - - bool argsOK = false; - - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &r, &g, &b, &a) ) - { - argsOK = true; - } - if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &startX, &startY, &width, &height, &r, &g, &b) ) - { - argsOK = true; - } - if ( argCount == 4 && PyArg_ParseTuple(args, "iiii",&r, &g, &b, &a) ) - { - argsOK = true; - } - if ( argCount == 3 && PyArg_ParseTuple(args, "iii",&r, &g, &b) ) - { - argsOK = true; - } - - if (argsOK) - { - QRect myQRect(startX,startY,width,height); - effect->_painter->fillRect(myQRect, QColor(r,g,b,a)); - return Py_BuildValue(""); - } - return nullptr; -} - - -PyObject* Effect::wrapImageDrawLine(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int r, g, b; - int a = 255; - int startX = 0; - int startY = 0; - int thick = 1; - int endX = effect->_imageSize.width(); - int endY = effect->_imageSize.height(); - - bool argsOK = false; - - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b, &a) ) - { - argsOK = true; - } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b) ) - { - argsOK = true; - } - - if (argsOK) - { - QPainter * painter = effect->_painter; - QRect myQRect(startX, startY, endX, endY); - QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); - newPen.setWidth(thick); - painter->setPen(newPen); - painter->drawLine(startX, startY, endX, endY); - painter->setPen(oldPen); - - return Py_BuildValue(""); - } - return nullptr; -} - -PyObject* Effect::wrapImageDrawPoint(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int r, g, b, x, y; - int a = 255; - int thick = 1; - - bool argsOK = false; - - if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &x, &y, &thick, &r, &g, &b, &a) ) - { - argsOK = true; - } - if ( argCount == 6 && PyArg_ParseTuple(args, "iiiiii", &x, &y, &thick, &r, &g, &b) ) - { - argsOK = true; - } - - if (argsOK) - { - QPainter * painter = effect->_painter; - QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); - newPen.setWidth(thick); - painter->setPen(newPen); - painter->drawPoint(x, y); - painter->setPen(oldPen); - - return Py_BuildValue(""); - } - return nullptr; -} - -PyObject* Effect::wrapImageDrawRect(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int r, g, b; - int a = 255; - int startX = 0; - int startY = 0; - int thick = 1; - int width = effect->_imageSize.width(); - int height = effect->_imageSize.height(); - - bool argsOK = false; - - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b, &a) ) - { - argsOK = true; - } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b) ) - { - argsOK = true; - } - - if (argsOK) - { - QPainter * painter = effect->_painter; - QRect myQRect(startX,startY,width,height); - QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); - newPen.setWidth(thick); - painter->setPen(newPen); - painter->drawRect(startX, startY, width, height); - painter->setPen(oldPen); - - return Py_BuildValue(""); - } - return nullptr; -} - - -PyObject* Effect::wrapImageSetPixel(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int r, g, b, x, y; - - if ( argCount == 5 && PyArg_ParseTuple(args, "iiiii", &x, &y, &r, &g, &b ) ) - { - effect->_image.setPixel(x,y,qRgb(r,g,b)); - return Py_BuildValue(""); - } - - return nullptr; -} - - -PyObject* Effect::wrapImageGetPixel(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int x, y; - - if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &x, &y) ) - { - QRgb rgb = effect->_image.pixel(x,y); - return Py_BuildValue("iii",qRed(rgb),qGreen(rgb),qBlue(rgb)); - } - return nullptr; -} - -PyObject* Effect::wrapImageSave(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - QImage img(effect->_image.copy()); - effect->_imageStack.append(img); - - return Py_BuildValue("i", effect->_imageStack.size()-1); -} - -PyObject* Effect::wrapImageMinSize(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int w, h; - int width = effect->_imageSize.width(); - int height = effect->_imageSize.height(); - - if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &w, &h) ) - { - if (width_painter; - - effect->_image = effect->_image.scaled(qMax(width,w),qMax(height,h), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - effect->_imageSize = effect->_image.size(); - effect->_painter = new QPainter(&(effect->_image)); - } - return Py_BuildValue("ii", effect->_image.width(), effect->_image.height()); - } - return nullptr; -} - -PyObject* Effect::wrapImageWidth(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - return Py_BuildValue("i", effect->_imageSize.width()); -} - -PyObject* Effect::wrapImageHeight(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - return Py_BuildValue("i", effect->_imageSize.height()); -} - -PyObject* Effect::wrapImageCRotate(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int argCount = PyTuple_Size(args); - int angle; - - if ( argCount == 1 && PyArg_ParseTuple(args, "i", &angle ) ) - { - angle = qMax(qMin(angle,360),0); - effect->_painter->rotate(angle); - return Py_BuildValue(""); - } - return nullptr; -} - -PyObject* Effect::wrapImageCOffset(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int offsetX = 0; - int offsetY = 0; - int argCount = PyTuple_Size(args); - - if ( argCount == 2 ) - { - PyArg_ParseTuple(args, "ii", &offsetX, &offsetY ); - } - - effect->_painter->translate(QPoint(offsetX,offsetY)); - return Py_BuildValue(""); -} - -PyObject* Effect::wrapImageCShear(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - int sh,sv; - int argCount = PyTuple_Size(args); - - if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &sh, &sv )) - { - effect->_painter->shear(sh,sv); - return Py_BuildValue(""); - } - return nullptr; -} - -PyObject* Effect::wrapImageResetT(PyObject *self, PyObject *args) -{ - Effect * effect = getEffect(); - - effect->_painter->resetTransform(); - return Py_BuildValue(""); -} - -Effect * Effect::getEffect() -{ - // extract the module from the runtime - PyObject * module = PyObject_GetAttrString(PyImport_AddModule("__main__"), "hyperion"); - - if (!PyModule_Check(module)) - { - // something is wrong - Py_XDECREF(module); - Error(Logger::getInstance("EFFECTENGINE"), "Unable to retrieve the effect object from the Python runtime"); - return nullptr; - } - - // retrieve the capsule with the effect - PyObject * effectCapsule = PyObject_GetAttrString(module, "__effectObj"); - Py_XDECREF(module); - - if (!PyCapsule_CheckExact(effectCapsule)) - { - // something is wrong - Py_XDECREF(effectCapsule); - Error(Logger::getInstance("EFFECTENGINE"), "Unable to retrieve the effect object from the Python runtime"); - return nullptr; - } - - // Get the effect from the capsule - Effect * effect = reinterpret_cast(PyCapsule_GetPointer(effectCapsule, nullptr)); - Py_XDECREF(effectCapsule); - return effect; -} diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp index bfa35c31..058b166c 100644 --- a/libsrc/effectengine/EffectEngine.cpp +++ b/libsrc/effectengine/EffectEngine.cpp @@ -16,16 +16,17 @@ // effect engine includes #include -#include "Effect.h" +#include +#include +#include #include "HyperionConfig.h" -EffectEngine::EffectEngine(Hyperion * hyperion, const QJsonObject & jsonEffectConfig) +EffectEngine::EffectEngine(Hyperion * hyperion) : _hyperion(hyperion) - , _effectConfig(jsonEffectConfig) , _availableEffects() , _activeEffects() , _log(Logger::getInstance("EFFECTENGINE")) - , _mainThreadState(nullptr) + , _effectFileHandler(EffectFileHandler::getInstance()) { Q_INIT_RESOURCE(EffectEngine); @@ -36,23 +37,25 @@ EffectEngine::EffectEngine(Hyperion * hyperion, const QJsonObject & jsonEffectCo connect(_hyperion, SIGNAL(channelCleared(int)), this, SLOT(channelCleared(int))); connect(_hyperion, SIGNAL(allChannelsCleared()), this, SLOT(allChannelsCleared())); - // read all effects - readEffects(); + // get notifications about refreshed effect list + connect(_effectFileHandler, &EffectFileHandler::effectListChanged, this, &EffectEngine::handleUpdatedEffectList); - // initialize the python interpreter - Debug(_log, "Initializing Python interpreter"); - Effect::registerHyperionExtensionModule(); - Py_InitializeEx(0); - PyEval_InitThreads(); // Create the GIL - _mainThreadState = PyEval_SaveThread(); + // register smooth cfgs and fill available effects + handleUpdatedEffectList(); } EffectEngine::~EffectEngine() { - // clean up the Python interpreter - Debug(_log, "Cleaning up Python interpreter"); - PyEval_RestoreThread(_mainThreadState); - Py_Finalize(); +} + +const bool EffectEngine::saveEffect(const QJsonObject& obj, QString& resultMsg) +{ + return _effectFileHandler->saveEffect(obj, resultMsg); +} + +const bool EffectEngine::deleteEffect(const QString& effectName, QString& resultMsg) +{ + return _effectFileHandler->deleteEffect(effectName, resultMsg); } const std::list &EffectEngine::getActiveEffects() @@ -73,174 +76,59 @@ const std::list &EffectEngine::getActiveEffects() return _availableActiveEffects; } -bool EffectEngine::loadEffectDefinition(const QString &path, const QString &effectConfigFile, EffectDefinition & effectDefinition) +const std::list & EffectEngine::getEffectSchemas() { - QString fileName = path + QDir::separator() + effectConfigFile; - - // Read and parse the effect json config file - QJsonObject configEffect; - if(!JsonUtils::readFile(fileName, configEffect, _log)) - return false; - - Q_INIT_RESOURCE(EffectEngine); - // validate effect config with effect schema(path) - if(!JsonUtils::validate(fileName, configEffect, ":effect-schema", _log)) - return false; - - // setup the definition - effectDefinition.file = fileName; - QJsonObject config = configEffect; - QString scriptName = config["script"].toString(); - effectDefinition.name = config["name"].toString(); - if (scriptName.isEmpty()) - return false; - - QFile fileInfo(scriptName); - - if (scriptName.mid(0, 1) == ":" ) - { - (!fileInfo.exists()) - ? effectDefinition.script = ":/effects/"+scriptName.mid(1) - : effectDefinition.script = scriptName; - } else - { - (!fileInfo.exists()) - ? effectDefinition.script = path + QDir::separator() + scriptName - : effectDefinition.script = scriptName; - } - - effectDefinition.args = config["args"].toObject(); - effectDefinition.smoothCfg = SMOOTHING_MODE_PAUSE; - if (effectDefinition.args["smoothing-custom-settings"].toBool()) - { - effectDefinition.smoothCfg = _hyperion->addSmoothingConfig( - effectDefinition.args["smoothing-time_ms"].toInt(), - effectDefinition.args["smoothing-updateFrequency"].toDouble(), - 0 ); - } - else - { - effectDefinition.smoothCfg = _hyperion->addSmoothingConfig(true); - } - return true; + return _effectFileHandler->getEffectSchemas(); } -bool EffectEngine::loadEffectSchema(const QString &path, const QString &effectSchemaFile, EffectSchema & effectSchema) +void EffectEngine::cacheRunningEffects() { - QString fileName = path + "schema/" + QDir::separator() + effectSchemaFile; + _cachedActiveEffects.clear(); - // Read and parse the effect schema file - QJsonObject schemaEffect; - if(!JsonUtils::readFile(fileName, schemaEffect, _log)) - return false; - - // setup the definition - QString scriptName = schemaEffect["script"].toString(); - effectSchema.schemaFile = fileName; - fileName = path + QDir::separator() + scriptName; - QFile pyFile(fileName); - - if (scriptName.isEmpty() || !pyFile.open(QIODevice::ReadOnly)) + for (Effect * effect : _activeEffects) { - fileName = path + "schema/" + QDir::separator() + effectSchemaFile; - Error( _log, "Python script '%s' in effect schema '%s' could not be loaded", QSTRING_CSTR(scriptName), QSTRING_CSTR(fileName)); - return false; + ActiveEffectDefinition activeEffectDefinition; + activeEffectDefinition.script = effect->getScript(); + activeEffectDefinition.name = effect->getName(); + activeEffectDefinition.priority = effect->getPriority(); + activeEffectDefinition.timeout = effect->getTimeout(); + activeEffectDefinition.args = effect->getArgs(); + _cachedActiveEffects.push_back(activeEffectDefinition); + channelCleared(effect->getPriority()); } - - pyFile.close(); - - effectSchema.pyFile = (scriptName.mid(0, 1) == ":" ) ? ":/effects/"+scriptName.mid(1) : path + QDir::separator() + scriptName; - effectSchema.pySchema = schemaEffect; - - return true; } -void EffectEngine::readEffects() +void EffectEngine::startCachedEffects() +{ + for (const auto & def : _cachedActiveEffects) + { + // the smooth cfg AND origin are ignored for this start! + runEffect(def.name, def.args, def.priority, def.timeout, def.script); + } + _cachedActiveEffects.clear(); +} + +void EffectEngine::handleUpdatedEffectList() { - // clear all lists _availableEffects.clear(); - _effectSchemas.clear(); - // read all effects - const QJsonArray & paths = _effectConfig["paths"].toArray(); - const QJsonArray & disabledEfx = _effectConfig["disable"].toArray(); - - QStringList efxPathList; - efxPathList << ":/effects/"; - QStringList disableList; - - for(auto p : paths) + for (auto def : _effectFileHandler->getEffects()) { - efxPathList << p.toString().replace("$ROOT",_hyperion->getRootPath()); - } - for(auto efx : disabledEfx) - { - disableList << efx.toString(); - } - - QMap availableEffects; - for (const QString & path : efxPathList ) - { - QDir directory(path); - if (!directory.exists()) + // add smoothing configs to Hyperion + if (def.args["smoothing-custom-settings"].toBool()) { - if(directory.mkpath(path)) - { - Warning(_log, "New Effect path \"%s\" created successfull", QSTRING_CSTR(path) ); - } - else - { - Warning(_log, "Failed to create Effect path \"%s\", please check permissions", QSTRING_CSTR(path) ); - } + def.smoothCfg = _hyperion->addSmoothingConfig( + def.args["smoothing-time_ms"].toInt(), + def.args["smoothing-updateFrequency"].toDouble(), + 0 ); } else { - int efxCount = 0; - QStringList filenames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); - for (const QString & filename : filenames) - { - EffectDefinition def; - if (loadEffectDefinition(path, filename, def)) - { - InfoIf(availableEffects.find(def.name) != availableEffects.end(), _log, - "effect overload effect '%s' is now taken from '%s'", QSTRING_CSTR(def.name), QSTRING_CSTR(path) ); - - if ( disableList.contains(def.name) ) - { - Info(_log, "effect '%s' not loaded, because it is disabled in hyperion config", QSTRING_CSTR(def.name)); - } - else - { - availableEffects[def.name] = def; - efxCount++; - } - } - } - Info(_log, "%d effects loaded from directory %s", efxCount, QSTRING_CSTR(path)); - - // collect effect schemas - efxCount = 0; - directory = path.endsWith("/") ? (path + "schema/") : (path + "/schema/"); - QStringList pynames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); - for (const QString & pyname : pynames) - { - EffectSchema pyEffect; - if (loadEffectSchema(path, pyname, pyEffect)) - { - _effectSchemas.push_back(pyEffect); - efxCount++; - } - } - InfoIf(efxCount > 0, _log, "%d effect schemas loaded from directory %s", efxCount, QSTRING_CSTR((path + "schema/"))); + def.smoothCfg = _hyperion->addSmoothingConfig(true); } + _availableEffects.push_back(def); } - - for(auto item : availableEffects) - { - _availableEffects.push_back(item); - } - - ErrorIf(_availableEffects.size()==0, _log, "no effects found, check your effect directories"); + emit effectListUpdated(); } int EffectEngine::runEffect(const QString &effectName, int priority, int timeout, const QString &origin) @@ -248,14 +136,14 @@ int EffectEngine::runEffect(const QString &effectName, int priority, int timeout return runEffect(effectName, QJsonObject(), priority, timeout, "", origin); } -int EffectEngine::runEffect(const QString &effectName, const QJsonObject &args, int priority, int timeout, const QString &pythonScript, const QString &origin, unsigned smoothCfg) +int EffectEngine::runEffect(const QString &effectName, const QJsonObject &args, int priority, int timeout, const QString &pythonScript, const QString &origin, unsigned smoothCfg, const QString &imageData) { Info( _log, "run effect %s on channel %d", QSTRING_CSTR(effectName), priority); if (pythonScript.isEmpty()) { - const EffectDefinition * effectDefinition = nullptr; - for (const EffectDefinition & e : _availableEffects) + const EffectDefinition *effectDefinition = nullptr; + for (const EffectDefinition &e : _availableEffects) { if (e.name == effectName) { @@ -266,28 +154,29 @@ int EffectEngine::runEffect(const QString &effectName, const QJsonObject &args, if (effectDefinition == nullptr) { // no such effect - Error(_log, "effect %s not found", QSTRING_CSTR(effectName)); + Error(_log, "Effect %s not found", QSTRING_CSTR(effectName)); return -1; } return runEffectScript(effectDefinition->script, effectName, (args.isEmpty() ? effectDefinition->args : args), priority, timeout, origin, effectDefinition->smoothCfg); } - return runEffectScript(pythonScript, effectName, args, priority, timeout, origin, smoothCfg); + return runEffectScript(pythonScript, effectName, args, priority, timeout, origin, smoothCfg, imageData); } -int EffectEngine::runEffectScript(const QString &script, const QString &name, const QJsonObject &args, int priority, int timeout, const QString & origin, unsigned smoothCfg) +int EffectEngine::runEffectScript(const QString &script, const QString &name, const QJsonObject &args, int priority, int timeout, const QString &origin, unsigned smoothCfg, const QString &imageData) { // clear current effect on the channel channelCleared(priority); // create the effect - Effect * effect = new Effect(_mainThreadState, priority, timeout, script, name, args, origin, smoothCfg); - connect(effect, SIGNAL(setColors(int,std::vector,int,bool,hyperion::Components,const QString,unsigned)), _hyperion, SLOT(setColors(int,std::vector,int,bool,hyperion::Components,const QString,unsigned)), Qt::QueuedConnection); + Effect *effect = new Effect(_hyperion, priority, timeout, script, name, args, imageData); + connect(effect, &Effect::setInput, _hyperion, &Hyperion::setInput, Qt::QueuedConnection); + connect(effect, &Effect::setInputImage, _hyperion, &Hyperion::setInputImage, Qt::QueuedConnection); connect(effect, &QThread::finished, this, &EffectEngine::effectFinished); _activeEffects.push_back(effect); // start the effect - _hyperion->registerPriority(name, priority); + _hyperion->registerInput(priority, hyperion::COMP_EFFECT, origin, name ,smoothCfg); effect->start(); return 0; @@ -299,7 +188,7 @@ void EffectEngine::channelCleared(int priority) { if (effect->getPriority() == priority) { - effect->requestInterruption(); + effect->setInteruptionFlag(); } } } @@ -310,7 +199,7 @@ void EffectEngine::allChannelsCleared() { if (effect->getPriority() != 254) { - effect->requestInterruption(); + effect->setInteruptionFlag(); } } } @@ -318,7 +207,7 @@ void EffectEngine::allChannelsCleared() void EffectEngine::effectFinished() { Effect* effect = qobject_cast(sender()); - if (!effect->isInterruptionRequested()) + if (!effect->hasInteruptionFlag()) { // effect stopped by itself. Clear the channel _hyperion->clear(effect->getPriority()); @@ -336,5 +225,4 @@ void EffectEngine::effectFinished() // cleanup the effect effect->deleteLater(); - _hyperion->unRegisterPriority(effect->getName()); } diff --git a/libsrc/effectengine/EffectFileHandler.cpp b/libsrc/effectengine/EffectFileHandler.cpp new file mode 100644 index 00000000..aa4d8800 --- /dev/null +++ b/libsrc/effectengine/EffectFileHandler.cpp @@ -0,0 +1,342 @@ +#include + +// util +#include + +// qt +#include +#include +#include +#include +#include + +// createEffect helper +struct find_schema: std::unary_function +{ + QString pyFile; + find_schema(QString pyFile):pyFile(pyFile) { } + bool operator()(EffectSchema const& schema) const + { + return schema.pyFile == pyFile; + } +}; + +// deleteEffect helper +struct find_effect: std::unary_function +{ + QString effectName; + find_effect(QString effectName) :effectName(effectName) { } + bool operator()(EffectDefinition const& effectDefinition) const + { + return effectDefinition.name == effectName; + } +}; + +EffectFileHandler* EffectFileHandler::efhInstance; + +EffectFileHandler::EffectFileHandler(const QString& rootPath, const QJsonDocument& effectConfig, QObject* parent) + : QObject(parent) + , _effectConfig() + , _log(Logger::getInstance("EFFECTFILES")) + , _rootPath(rootPath) +{ + EffectFileHandler::efhInstance = this; + + Q_INIT_RESOURCE(EffectEngine); + + // init + handleSettingsUpdate(settings::EFFECTS, effectConfig); +} + +void EffectFileHandler::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::EFFECTS) + { + _effectConfig = config.object(); + // update effects and schemas + updateEffects(); + } +} + +const bool EffectFileHandler::deleteEffect(const QString& effectName, QString& resultMsg) +{ + std::list effectsDefinition = getEffects(); + std::list::iterator it = std::find_if(effectsDefinition.begin(), effectsDefinition.end(), find_effect(effectName)); + + if (it != effectsDefinition.end()) + { + QFileInfo effectConfigurationFile(it->file); + if (effectConfigurationFile.absoluteFilePath().mid(0, 1) != ":" ) + { + if (effectConfigurationFile.exists()) + { + if ( (it->script == ":/effects/gif.py") && !it->args.value("image").toString("").isEmpty()) + { + QFileInfo effectImageFile(effectConfigurationFile.absolutePath() + "/" + it->args.value("image").toString()); + if (effectImageFile.exists()) + QFile::remove(effectImageFile.absoluteFilePath()); + } + + bool result = QFile::remove(effectConfigurationFile.absoluteFilePath()); + + if (result) + { + updateEffects(); + return true; + } else + resultMsg = "Can't delete effect configuration file: " + effectConfigurationFile.absoluteFilePath() + ". Please check permissions"; + } else + resultMsg = "Can't find effect configuration file: " + effectConfigurationFile.absoluteFilePath(); + } else + resultMsg = "Can't delete internal effect: " + effectName; + } else + resultMsg = "Effect " + effectName + " not found"; + + return false; +} + +const bool EffectFileHandler::saveEffect(const QJsonObject& message, QString& resultMsg) +{ + if (!message["args"].toObject().isEmpty()) + { + QString scriptName; + (message["script"].toString().mid(0, 1) == ":" ) + ? scriptName = ":/effects//" + message["script"].toString().mid(1) + : scriptName = message["script"].toString(); + + std::list effectsSchemas = getEffectSchemas(); + std::list::iterator it = std::find_if(effectsSchemas.begin(), effectsSchemas.end(), find_schema(scriptName)); + + if (it != effectsSchemas.end()) + { + if(!JsonUtils::validate("EffectFileHandler", message["args"].toObject(), it->schemaFile, _log)) + { + resultMsg = "Error during arg validation against schema, please consult the Hyperion Log"; + return false; + } + + QJsonObject effectJson; + QJsonArray effectArray; + effectArray = _effectConfig["paths"].toArray(); + + if (effectArray.size() > 0) + { + if (message["name"].toString().trimmed().isEmpty() || message["name"].toString().trimmed().startsWith(".")) + { + resultMsg = "Can't save new effect. Effect name is empty or begins with a dot."; + return false; + } + + effectJson["name"] = message["name"].toString(); + effectJson["script"] = message["script"].toString(); + effectJson["args"] = message["args"].toObject(); + + std::list availableEffects = getEffects(); + std::list::iterator iter = std::find_if(availableEffects.begin(), availableEffects.end(), find_effect(message["name"].toString())); + + QFileInfo newFileName; + if (iter != availableEffects.end()) + { + newFileName.setFile(iter->file); + if (newFileName.absoluteFilePath().mid(0, 1) == ":") + { + resultMsg = "The effect name '" + message["name"].toString() + "' is assigned to an internal effect. Please rename your effekt."; + return false; + } + } else + { + // TODO global special keyword handling + QString f = effectArray[0].toString().replace("$ROOT",_rootPath) + "/" + message["name"].toString().replace(QString(" "), QString("")) + QString(".json"); + newFileName.setFile(f); + } + + //TODO check if filename exist + if (!message["imageData"].toString("").isEmpty() && !message["args"].toObject().value("image").toString("").isEmpty()) + { + QFileInfo imageFileName(effectArray[0].toString().replace("$ROOT",_rootPath) + "/" + message["args"].toObject().value("image").toString()); + if(!FileUtils::writeFile(imageFileName.absoluteFilePath(), QByteArray::fromBase64(message["imageData"].toString("").toUtf8()), _log)) + { + resultMsg = "Error while saving image file '" + message["args"].toObject().value("image").toString() + ", please check the Hyperion Log"; + return false; + } + } + + if(!JsonUtils::write(newFileName.absoluteFilePath(), effectJson, _log)) + { + resultMsg = "Error while saving effect, please check the Hyperion Log"; + return false; + } + + Info(_log, "Reload effect list"); + updateEffects(); + resultMsg = ""; + return true; + } else + resultMsg = "Can't save new effect. Effect path empty"; + } else + resultMsg = "Missing schema file for Python script " + message["script"].toString(); + } else + resultMsg = "Missing or empty Object 'args'"; + + return false; +} + +void EffectFileHandler::updateEffects() +{ + // clear all lists + _availableEffects.clear(); + _effectSchemas.clear(); + + // read all effects + const QJsonArray & paths = _effectConfig["paths"].toArray(); + const QJsonArray & disabledEfx = _effectConfig["disable"].toArray(); + + QStringList efxPathList; + efxPathList << ":/effects/"; + QStringList disableList; + + for(auto p : paths) + { + efxPathList << p.toString().replace("$ROOT",_rootPath); + } + for(auto efx : disabledEfx) + { + disableList << efx.toString(); + } + + QMap availableEffects; + for (const QString & path : efxPathList ) + { + QDir directory(path); + if (!directory.exists()) + { + if(directory.mkpath(path)) + { + Info(_log, "New Effect path \"%s\" created successfull", QSTRING_CSTR(path) ); + } + else + { + Warning(_log, "Failed to create Effect path \"%s\", please check permissions", QSTRING_CSTR(path) ); + } + } + else + { + int efxCount = 0; + QStringList filenames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); + for (const QString & filename : filenames) + { + EffectDefinition def; + if (loadEffectDefinition(path, filename, def)) + { + InfoIf(availableEffects.find(def.name) != availableEffects.end(), _log, + "effect overload effect '%s' is now taken from '%s'", QSTRING_CSTR(def.name), QSTRING_CSTR(path) ); + + if ( disableList.contains(def.name) ) + { + Info(_log, "effect '%s' not loaded, because it is disabled in hyperion config", QSTRING_CSTR(def.name)); + } + else + { + availableEffects[def.name] = def; + efxCount++; + } + } + } + Info(_log, "%d effects loaded from directory %s", efxCount, QSTRING_CSTR(path)); + + // collect effect schemas + efxCount = 0; + directory = path.endsWith("/") ? (path + "schema/") : (path + "/schema/"); + QStringList pynames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); + for (const QString & pyname : pynames) + { + EffectSchema pyEffect; + if (loadEffectSchema(path, pyname, pyEffect)) + { + _effectSchemas.push_back(pyEffect); + efxCount++; + } + } + InfoIf(efxCount > 0, _log, "%d effect schemas loaded from directory %s", efxCount, QSTRING_CSTR((path + "schema/"))); + } + } + + for(auto item : availableEffects) + { + _availableEffects.push_back(item); + } + + ErrorIf(_availableEffects.size()==0, _log, "no effects found, check your effect directories"); + + emit effectListChanged(); +} + +bool EffectFileHandler::loadEffectDefinition(const QString &path, const QString &effectConfigFile, EffectDefinition & effectDefinition) +{ + QString fileName = path + QDir::separator() + effectConfigFile; + + // Read and parse the effect json config file + QJsonObject configEffect; + if(!JsonUtils::readFile(fileName, configEffect, _log)) + return false; + + // validate effect config with effect schema(path) + if(!JsonUtils::validate(fileName, configEffect, ":effect-schema", _log)) + return false; + + // setup the definition + effectDefinition.file = fileName; + QJsonObject config = configEffect; + QString scriptName = config["script"].toString(); + effectDefinition.name = config["name"].toString(); + if (scriptName.isEmpty()) + return false; + + QFile fileInfo(scriptName); + + if (scriptName.mid(0, 1) == ":" ) + { + (!fileInfo.exists()) + ? effectDefinition.script = ":/effects/"+scriptName.mid(1) + : effectDefinition.script = scriptName; + } else + { + (!fileInfo.exists()) + ? effectDefinition.script = path + QDir::separator() + scriptName + : effectDefinition.script = scriptName; + } + + effectDefinition.args = config["args"].toObject(); + effectDefinition.smoothCfg = 1; // pause config + return true; +} + +bool EffectFileHandler::loadEffectSchema(const QString &path, const QString &effectSchemaFile, EffectSchema & effectSchema) +{ + QString fileName = path + "schema/" + QDir::separator() + effectSchemaFile; + + // Read and parse the effect schema file + QJsonObject schemaEffect; + if(!JsonUtils::readFile(fileName, schemaEffect, _log)) + return false; + + // setup the definition + QString scriptName = schemaEffect["script"].toString(); + effectSchema.schemaFile = fileName; + fileName = path + QDir::separator() + scriptName; + QFile pyFile(fileName); + + if (scriptName.isEmpty() || !pyFile.open(QIODevice::ReadOnly)) + { + fileName = path + "schema/" + QDir::separator() + effectSchemaFile; + Error( _log, "Python script '%s' in effect schema '%s' could not be loaded", QSTRING_CSTR(scriptName), QSTRING_CSTR(fileName)); + return false; + } + + pyFile.close(); + + effectSchema.pyFile = (scriptName.mid(0, 1) == ":" ) ? ":/effects/"+scriptName.mid(1) : path + QDir::separator() + scriptName; + effectSchema.pySchema = schemaEffect; + + return true; +} diff --git a/libsrc/effectengine/EffectModule.cpp b/libsrc/effectengine/EffectModule.cpp new file mode 100644 index 00000000..d227d3d6 --- /dev/null +++ b/libsrc/effectengine/EffectModule.cpp @@ -0,0 +1,1095 @@ +#include + +#include +#include + +// hyperion +#include +#include + +// qt +#include +#include +#include +#include + +// create the hyperion module +struct PyModuleDef EffectModule::moduleDef = { + PyModuleDef_HEAD_INIT, + "hyperion", /* m_name */ + "Hyperion module", /* m_doc */ + -1, /* m_size */ + EffectModule::effectMethods, /* m_methods */ + NULL, /* m_reload */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; + +PyObject* EffectModule::PyInit_hyperion() +{ + return PyModule_Create(&moduleDef); +} + +void EffectModule::registerHyperionExtensionModule() +{ + PyImport_AppendInittab("hyperion", &PyInit_hyperion); +} + +PyObject *EffectModule::json2python(const QJsonValue &jsonData) +{ + switch (jsonData.type()) + { + case QJsonValue::Null: + return Py_BuildValue(""); + case QJsonValue::Undefined: + return Py_BuildValue(""); + case QJsonValue::Double: + { + if (std::round(jsonData.toDouble()) != jsonData.toDouble()) + return Py_BuildValue("d", jsonData.toDouble()); + + return Py_BuildValue("i", jsonData.toInt()); + } + case QJsonValue::Bool: + return Py_BuildValue("i", jsonData.toBool() ? 1 : 0); + case QJsonValue::String: + return Py_BuildValue("s", jsonData.toString().toUtf8().constData()); + case QJsonValue::Object: + { + PyObject * dict= PyDict_New(); + QJsonObject objectData = jsonData.toObject(); + for (QJsonObject::iterator i = objectData.begin(); i != objectData.end(); ++i) + { + PyObject * obj = json2python(*i); + PyDict_SetItemString(dict, i.key().toStdString().c_str(), obj); + Py_XDECREF(obj); + } + return dict; + } + case QJsonValue::Array: + { + QJsonArray arrayData = jsonData.toArray(); + PyObject * list = PyList_New(arrayData.size()); + int index = 0; + for (QJsonArray::iterator i = arrayData.begin(); i != arrayData.end(); ++i, ++index) + { + PyObject * obj = json2python(*i); + Py_INCREF(obj); + PyList_SetItem(list, index, obj); + Py_XDECREF(obj); + } + return list; + } + } + + assert(false); + return nullptr; +} + +// Python method table +PyMethodDef EffectModule::effectMethods[] = { + {"setColor" , EffectModule::wrapSetColor , METH_VARARGS, "Set a new color for the leds."}, + {"setImage" , EffectModule::wrapSetImage , METH_VARARGS, "Set a new image to process and determine new led colors."}, + {"getImage" , EffectModule::wrapGetImage , METH_VARARGS, "get image data from file."}, + {"abort" , EffectModule::wrapAbort , METH_NOARGS, "Check if the effect should abort execution."}, + {"imageShow" , EffectModule::wrapImageShow , METH_VARARGS, "set current effect image to hyperion core."}, + {"imageLinearGradient" , EffectModule::wrapImageLinearGradient , METH_VARARGS, ""}, + {"imageConicalGradient" , EffectModule::wrapImageConicalGradient , METH_VARARGS, ""}, + {"imageRadialGradient" , EffectModule::wrapImageRadialGradient , METH_VARARGS, ""}, + {"imageSolidFill" , EffectModule::wrapImageSolidFill , METH_VARARGS, ""}, + {"imageDrawLine" , EffectModule::wrapImageDrawLine , METH_VARARGS, ""}, + {"imageDrawPoint" , EffectModule::wrapImageDrawPoint , METH_VARARGS, ""}, + {"imageDrawRect" , EffectModule::wrapImageDrawRect , METH_VARARGS, ""}, + {"imageDrawPolygon" , EffectModule::wrapImageDrawPolygon , METH_VARARGS, ""}, + {"imageDrawPie" , EffectModule::wrapImageDrawPie , METH_VARARGS, ""}, + {"imageSetPixel" , EffectModule::wrapImageSetPixel , METH_VARARGS, "set pixel color of image"}, + {"imageGetPixel" , EffectModule::wrapImageGetPixel , METH_VARARGS, "get pixel color of image"}, + {"imageSave" , EffectModule::wrapImageSave , METH_NOARGS, "adds a new background image"}, + {"imageMinSize" , EffectModule::wrapImageMinSize , METH_VARARGS, "sets minimal dimension of background image"}, + {"imageWidth" , EffectModule::wrapImageWidth , METH_NOARGS, "gets image width"}, + {"imageHeight" , EffectModule::wrapImageHeight , METH_NOARGS, "gets image height"}, + {"imageCRotate" , EffectModule::wrapImageCRotate , METH_VARARGS, "rotate the coordinate system by given angle"}, + {"imageCOffset" , EffectModule::wrapImageCOffset , METH_VARARGS, "Add offset to the coordinate system"}, + {"imageCShear" , EffectModule::wrapImageCShear , METH_VARARGS, "Shear of coordinate system by the given horizontal/vertical axis"}, + {"imageResetT" , EffectModule::wrapImageResetT , METH_NOARGS, "Resets all coords modifications (rotate,offset,shear)"}, + {NULL, NULL, 0, NULL} +}; + +PyObject* EffectModule::wrapSetColor(PyObject *self, PyObject *args) +{ + // get the effect + Effect * effect = getEffect(); + + // check if we have aborted already + if (effect->hasInteruptionFlag()) + { + return Py_BuildValue(""); + } + + // determine the timeout + int timeout = effect->_timeout; + if (timeout > 0) + { + timeout = effect->_endTime - QDateTime::currentMSecsSinceEpoch(); + + // we are done if the time has passed + if (timeout <= 0) + { + return Py_BuildValue(""); + } + } + + // check the number of arguments + int argCount = PyTuple_Size(args); + if (argCount == 3) + { + // three seperate arguments for red, green, and blue + ColorRgb color; + if (PyArg_ParseTuple(args, "bbb", &color.red, &color.green, &color.blue)) + { + effect->_colors.fill(color); + effect->setInput(effect->_priority, effect->_colors.toStdVector(), timeout, false); + return Py_BuildValue(""); + } + return nullptr; + } + else if (argCount == 1) + { + // bytearray of values + PyObject * bytearray = nullptr; + if (PyArg_ParseTuple(args, "O", &bytearray)) + { + if (PyByteArray_Check(bytearray)) + { + size_t length = PyByteArray_Size(bytearray); + if (length == 3 * effect->_hyperion->getLedCount()) + { + char * data = PyByteArray_AS_STRING(bytearray); + memcpy(effect->_colors.data(), data, length); + effect->setInput(effect->_priority, effect->_colors.toStdVector(), timeout, false); + return Py_BuildValue(""); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should be 3*ledCount"); + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Argument is not a bytearray"); + return nullptr; + } + } + else + { + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Function expect 1 or 3 arguments"); + return nullptr; + } +} + +PyObject* EffectModule::wrapSetImage(PyObject *self, PyObject *args) +{ + // get the effect + Effect * effect = getEffect(); + + // check if we have aborted already + if (effect->hasInteruptionFlag()) + { + return Py_BuildValue(""); + } + + // determine the timeout + int timeout = effect->_timeout; + if (timeout > 0) + { + timeout = effect->_endTime - QDateTime::currentMSecsSinceEpoch(); + + // we are done if the time has passed + if (timeout <= 0) + { + return Py_BuildValue(""); + } + } + + // bytearray of values + int width, height; + PyObject * bytearray = nullptr; + if (PyArg_ParseTuple(args, "iiO", &width, &height, &bytearray)) + { + if (PyByteArray_Check(bytearray)) + { + int length = PyByteArray_Size(bytearray); + if (length == 3 * width * height) + { + Image image(width, height); + char * data = PyByteArray_AS_STRING(bytearray); + memcpy(image.memptr(), data, length); + effect->setInputImage(effect->_priority, image, timeout, false); + return Py_BuildValue(""); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should be 3*width*height"); + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Argument 3 is not a bytearray"); + return nullptr; + } + } + else + { + return nullptr; + } + + // error + PyErr_SetString(PyExc_RuntimeError, "Unknown error"); + return nullptr; +} + +PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) +{ + Effect *effect = getEffect(); + + QString file; + QBuffer buffer; + QImageReader reader; + + if (effect->_imageData.isEmpty()) + { + Q_INIT_RESOURCE(EffectEngine); + + char *source; + if(!PyArg_ParseTuple(args, "s", &source)) + { + PyErr_SetString(PyExc_TypeError, "String required"); + return NULL; + } + + file = QString::fromUtf8(source); + + if (file.mid(0, 1) == ":") + file = ":/effects/"+file.mid(1); + + reader.setDecideFormatFromContent(true); + reader.setFileName(file); + } + else + { + buffer.setData(QByteArray::fromBase64(effect->_imageData.toUtf8())); + buffer.open(QBuffer::ReadOnly); + reader.setDecideFormatFromContent(true); + reader.setDevice(&buffer); + } + + if (reader.canRead()) + { + PyObject *result = PyList_New(reader.imageCount()); + + for (int i = 0; i < reader.imageCount(); ++i) + { + reader.jumpToImage(i); + if (reader.canRead()) + { + QImage qimage = reader.read(); + + int width = qimage.width(); + int height = qimage.height(); + + QByteArray binaryImage; + for (int i = 0; i(qimage.scanLine(i)); + for (int j = 0; j< width; ++j) + { + binaryImage.append((char) qRed(scanline[j])); + binaryImage.append((char) qGreen(scanline[j])); + binaryImage.append((char) qBlue(scanline[j])); + } + } + PyList_SET_ITEM(result, i, Py_BuildValue("{s:i,s:i,s:O}", "imageWidth", width, "imageHeight", height, "imageData", PyByteArray_FromStringAndSize(binaryImage.constData(),binaryImage.size()))); + } + else + { + PyErr_SetString(PyExc_TypeError, reader.errorString().toUtf8().constData()); + return NULL; + } + } + return result; + } + else + { + PyErr_SetString(PyExc_TypeError, reader.errorString().toUtf8().constData()); + return NULL; + } +} + +PyObject* EffectModule::wrapAbort(PyObject *self, PyObject *) +{ + Effect * effect = getEffect(); + + // Test if the effect has reached it end time + if (effect->_timeout > 0 && QDateTime::currentMSecsSinceEpoch() > effect->_endTime) + { + effect->setInteruptionFlag(); + } + + return Py_BuildValue("i", effect->hasInteruptionFlag() ? 1 : 0); +} + + +PyObject* EffectModule::wrapImageShow(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + // determine the timeout + int timeout = effect->_timeout; + if (timeout > 0) + { + timeout = effect->_endTime - QDateTime::currentMSecsSinceEpoch(); + + // we are done if the time has passed + if (timeout <= 0) + { + return Py_BuildValue(""); + } + } + + int argCount = PyTuple_Size(args); + int imgId = -1; + bool argsOk = (argCount == 0); + if (argCount == 1 && PyArg_ParseTuple(args, "i", &imgId)) + { + argsOk = true; + } + + if ( ! argsOk || (imgId>-1 && imgId >= effect->_imageStack.size())) + { + return nullptr; + } + + + QImage * qimage = (imgId<0) ? &(effect->_image) : &(effect->_imageStack[imgId]); + int width = qimage->width(); + int height = qimage->height(); + + Image image(width, height); + QByteArray binaryImage; + + for (int i = 0; i(qimage->scanLine(i)); + for (int j = 0; j< width; ++j) + { + binaryImage.append((char) qRed(scanline[j])); + binaryImage.append((char) qGreen(scanline[j])); + binaryImage.append((char) qBlue(scanline[j])); + } + } + + memcpy(image.memptr(), binaryImage.data(), binaryImage.size()); + effect->setInputImage(effect->_priority, image, timeout, false); + + return Py_BuildValue(""); +} + +PyObject* EffectModule::wrapImageLinearGradient(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + PyObject * bytearray = nullptr; + int startRX = 0; + int startRY = 0; + int startX = 0; + int startY = 0; + int endX, width = effect->_imageSize.width(); + int endY, height = effect->_imageSize.height(); + int spread = 0; + + bool argsOK = false; + + if ( argCount == 10 && PyArg_ParseTuple(args, "iiiiiiiiOi", &startRX, &startRY, &width, &height, &startX, &startY, &endX, &endY, &bytearray, &spread) ) + { + argsOK = true; + } + if ( argCount == 6 && PyArg_ParseTuple(args, "iiiiOi", &startX, &startY, &endX, &endY, &bytearray, &spread) ) + { + argsOK = true; + } + + if (argsOK) + { + if (PyByteArray_Check(bytearray)) + { + const int length = PyByteArray_Size(bytearray); + const unsigned arrayItemLength = 5; + if (length % arrayItemLength == 0) + { + QRect myQRect(startRX,startRY,width,height); + QLinearGradient gradient(QPoint(startX,startY), QPoint(endX,endY)); + char * data = PyByteArray_AS_STRING(bytearray); + + for (int idx=0; idx(spread)); + effect->_painter->fillRect(myQRect, gradient); + + return Py_BuildValue(""); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 5"); + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "No bytearray properly defined"); + return nullptr; + } + } + return nullptr; +} + +PyObject* EffectModule::wrapImageConicalGradient(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + PyObject * bytearray = nullptr; + int centerX, centerY, angle; + int startX = 0; + int startY = 0; + int width = effect->_imageSize.width(); + int height = effect->_imageSize.height(); + + bool argsOK = false; + + if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiiO", &startX, &startY, &width, &height, ¢erX, ¢erY, &angle, &bytearray) ) + { + argsOK = true; + } + if ( argCount == 4 && PyArg_ParseTuple(args, "iiiO", ¢erX, ¢erY, &angle, &bytearray) ) + { + argsOK = true; + } + angle = qMax(qMin(angle,360),0); + + if (argsOK) + { + if (PyByteArray_Check(bytearray)) + { + const int length = PyByteArray_Size(bytearray); + const unsigned arrayItemLength = 5; + if (length % arrayItemLength == 0) + { + QRect myQRect(startX,startY,width,height); + QConicalGradient gradient(QPoint(centerX,centerY), angle ); + char * data = PyByteArray_AS_STRING(bytearray); + + for (int idx=0; idx_painter->fillRect(myQRect, gradient); + + return Py_BuildValue(""); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 5"); + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Argument 8 is not a bytearray"); + return nullptr; + } + } + return nullptr; +} + + +PyObject* EffectModule::wrapImageRadialGradient(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + PyObject * bytearray = nullptr; + int centerX, centerY, radius, focalX, focalY, focalRadius, spread; + int startX = 0; + int startY = 0; + int width = effect->_imageSize.width(); + int height = effect->_imageSize.height(); + + bool argsOK = false; + + if ( argCount == 12 && PyArg_ParseTuple(args, "iiiiiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread) ) + { + argsOK = true; + } + if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &bytearray, &spread) ) + { + argsOK = true; + focalX = centerX; + focalY = centerY; + focalRadius = radius; + } + if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiOi", ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread) ) + { + argsOK = true; + } + if ( argCount == 5 && PyArg_ParseTuple(args, "iiiOi", ¢erX, ¢erY, &radius, &bytearray, &spread) ) + { + argsOK = true; + focalX = centerX; + focalY = centerY; + focalRadius = radius; + } + + if (argsOK) + { + if (PyByteArray_Check(bytearray)) + { + int length = PyByteArray_Size(bytearray); + if (length % 4 == 0) + { + + QRect myQRect(startX,startY,width,height); + QRadialGradient gradient(QPoint(centerX,centerY), qMax(radius,0) ); + char * data = PyByteArray_AS_STRING(bytearray); + + for (int idx=0; idx(spread)); + effect->_painter->fillRect(myQRect, gradient); + + return Py_BuildValue(""); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 4"); + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Last argument is not a bytearray"); + return nullptr; + } + } + return nullptr; +} + +PyObject* EffectModule::wrapImageDrawPolygon(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + PyObject * bytearray = nullptr; + + int argCount = PyTuple_Size(args); + int r, g, b; + int a = 255; + + bool argsOK = false; + + if ( argCount == 5 && PyArg_ParseTuple(args, "Oiiii", &bytearray, &r, &g, &b, &a) ) + { + argsOK = true; + } + if ( argCount == 4 && PyArg_ParseTuple(args, "Oiii", &bytearray, &r, &g, &b) ) + { + argsOK = true; + } + + if (argsOK) + { + if (PyByteArray_Check(bytearray)) + { + int length = PyByteArray_Size(bytearray); + if (length % 2 == 0) + { + QVector points; + char * data = PyByteArray_AS_STRING(bytearray); + + for (int idx=0; idx_painter; + QPen oldPen = painter->pen(); + QPen newPen(QColor(r,g,b,a)); + painter->setPen(newPen); + painter->setBrush(QBrush(QColor(r,g,b,a), Qt::SolidPattern)); + painter->drawPolygon(points); + painter->setPen(oldPen); + return Py_BuildValue(""); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 2"); + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Argument 1 is not a bytearray"); + return nullptr; + } + } + return nullptr; +} + +PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + PyObject * bytearray = nullptr; + + QString brush; + int argCount = PyTuple_Size(args); + int radius, centerX, centerY; + int startAngle = 0; + int spanAngle = 360; + int r = 0; + int g = 0; + int b = 0; + int a = 255; + + bool argsOK = false; + + if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b, &a) ) + { + argsOK = true; + } + if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b) ) + { + argsOK = true; + } + if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiisO", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &brush, &bytearray) ) + { + argsOK = true; + } + if ( argCount == 5 && PyArg_ParseTuple(args, "iiisO", ¢erX, ¢erY, &radius, &brush, &bytearray) ) + { + argsOK = true; + } + + if (argsOK) + { + QPainter * painter = effect->_painter; + startAngle = qMax(qMin(startAngle,360),0); + spanAngle = qMax(qMin(spanAngle,360),-360); + + if( argCount == 7 || argCount == 5 ) + { + a = 0; + if (PyByteArray_Check(bytearray)) + { + int length = PyByteArray_Size(bytearray); + if (length % 5 == 0) + { + + QConicalGradient gradient(QPoint(centerX,centerY), startAngle); + + + char * data = PyByteArray_AS_STRING(bytearray); + + for (int idx=0; idxsetBrush(gradient); + + return Py_BuildValue(""); + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Length of bytearray argument should multiple of 5"); + return nullptr; + } + } + else + { + PyErr_SetString(PyExc_RuntimeError, "Last argument is not a bytearray"); + return nullptr; + } + } + else + { + painter->setBrush(QBrush(QColor(r,g,b,a), Qt::SolidPattern)); + } + QPen oldPen = painter->pen(); + QPen newPen(QColor(r,g,b,a)); + painter->setPen(newPen); + painter->drawPie(centerX - radius, centerY - radius, centerX + radius, centerY + radius, startAngle * 16, spanAngle * 16); + painter->setPen(oldPen); + return Py_BuildValue(""); + } + return nullptr; +} + +PyObject* EffectModule::wrapImageSolidFill(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int r, g, b; + int a = 255; + int startX = 0; + int startY = 0; + int width = effect->_imageSize.width(); + int height = effect->_imageSize.height(); + + bool argsOK = false; + + if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &r, &g, &b, &a) ) + { + argsOK = true; + } + if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &startX, &startY, &width, &height, &r, &g, &b) ) + { + argsOK = true; + } + if ( argCount == 4 && PyArg_ParseTuple(args, "iiii",&r, &g, &b, &a) ) + { + argsOK = true; + } + if ( argCount == 3 && PyArg_ParseTuple(args, "iii",&r, &g, &b) ) + { + argsOK = true; + } + + if (argsOK) + { + QRect myQRect(startX,startY,width,height); + effect->_painter->fillRect(myQRect, QColor(r,g,b,a)); + return Py_BuildValue(""); + } + return nullptr; +} + + +PyObject* EffectModule::wrapImageDrawLine(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int r, g, b; + int a = 255; + int startX = 0; + int startY = 0; + int thick = 1; + int endX = effect->_imageSize.width(); + int endY = effect->_imageSize.height(); + + bool argsOK = false; + + if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b, &a) ) + { + argsOK = true; + } + if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b) ) + { + argsOK = true; + } + + if (argsOK) + { + QPainter * painter = effect->_painter; + QRect myQRect(startX, startY, endX, endY); + QPen oldPen = painter->pen(); + QPen newPen(QColor(r,g,b,a)); + newPen.setWidth(thick); + painter->setPen(newPen); + painter->drawLine(startX, startY, endX, endY); + painter->setPen(oldPen); + + return Py_BuildValue(""); + } + return nullptr; +} + +PyObject* EffectModule::wrapImageDrawPoint(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int r, g, b, x, y; + int a = 255; + int thick = 1; + + bool argsOK = false; + + if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &x, &y, &thick, &r, &g, &b, &a) ) + { + argsOK = true; + } + if ( argCount == 6 && PyArg_ParseTuple(args, "iiiiii", &x, &y, &thick, &r, &g, &b) ) + { + argsOK = true; + } + + if (argsOK) + { + QPainter * painter = effect->_painter; + QPen oldPen = painter->pen(); + QPen newPen(QColor(r,g,b,a)); + newPen.setWidth(thick); + painter->setPen(newPen); + painter->drawPoint(x, y); + painter->setPen(oldPen); + + return Py_BuildValue(""); + } + return nullptr; +} + +PyObject* EffectModule::wrapImageDrawRect(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int r, g, b; + int a = 255; + int startX = 0; + int startY = 0; + int thick = 1; + int width = effect->_imageSize.width(); + int height = effect->_imageSize.height(); + + bool argsOK = false; + + if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b, &a) ) + { + argsOK = true; + } + if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b) ) + { + argsOK = true; + } + + if (argsOK) + { + QPainter * painter = effect->_painter; + QRect myQRect(startX,startY,width,height); + QPen oldPen = painter->pen(); + QPen newPen(QColor(r,g,b,a)); + newPen.setWidth(thick); + painter->setPen(newPen); + painter->drawRect(startX, startY, width, height); + painter->setPen(oldPen); + + return Py_BuildValue(""); + } + return nullptr; +} + + +PyObject* EffectModule::wrapImageSetPixel(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int r, g, b, x, y; + + if ( argCount == 5 && PyArg_ParseTuple(args, "iiiii", &x, &y, &r, &g, &b ) ) + { + effect->_image.setPixel(x,y,qRgb(r,g,b)); + return Py_BuildValue(""); + } + + return nullptr; +} + + +PyObject* EffectModule::wrapImageGetPixel(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int x, y; + + if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &x, &y) ) + { + QRgb rgb = effect->_image.pixel(x,y); + return Py_BuildValue("iii",qRed(rgb),qGreen(rgb),qBlue(rgb)); + } + return nullptr; +} + +PyObject* EffectModule::wrapImageSave(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + QImage img(effect->_image.copy()); + effect->_imageStack.append(img); + + return Py_BuildValue("i", effect->_imageStack.size()-1); +} + +PyObject* EffectModule::wrapImageMinSize(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int w, h; + int width = effect->_imageSize.width(); + int height = effect->_imageSize.height(); + + if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &w, &h) ) + { + if (width_painter; + + effect->_image = effect->_image.scaled(qMax(width,w),qMax(height,h), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + effect->_imageSize = effect->_image.size(); + effect->_painter = new QPainter(&(effect->_image)); + } + return Py_BuildValue("ii", effect->_image.width(), effect->_image.height()); + } + return nullptr; +} + +PyObject* EffectModule::wrapImageWidth(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + return Py_BuildValue("i", effect->_imageSize.width()); +} + +PyObject* EffectModule::wrapImageHeight(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + return Py_BuildValue("i", effect->_imageSize.height()); +} + +PyObject* EffectModule::wrapImageCRotate(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int argCount = PyTuple_Size(args); + int angle; + + if ( argCount == 1 && PyArg_ParseTuple(args, "i", &angle ) ) + { + angle = qMax(qMin(angle,360),0); + effect->_painter->rotate(angle); + return Py_BuildValue(""); + } + return nullptr; +} + +PyObject* EffectModule::wrapImageCOffset(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int offsetX = 0; + int offsetY = 0; + int argCount = PyTuple_Size(args); + + if ( argCount == 2 ) + { + PyArg_ParseTuple(args, "ii", &offsetX, &offsetY ); + } + + effect->_painter->translate(QPoint(offsetX,offsetY)); + return Py_BuildValue(""); +} + +PyObject* EffectModule::wrapImageCShear(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + int sh,sv; + int argCount = PyTuple_Size(args); + + if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &sh, &sv )) + { + effect->_painter->shear(sh,sv); + return Py_BuildValue(""); + } + return nullptr; +} + +PyObject* EffectModule::wrapImageResetT(PyObject *self, PyObject *args) +{ + Effect * effect = getEffect(); + + effect->_painter->resetTransform(); + return Py_BuildValue(""); +} + +Effect * EffectModule::getEffect() +{ + // extract the module from the runtime + PyObject * module = PyObject_GetAttrString(PyImport_AddModule("__main__"), "hyperion"); + + if (!PyModule_Check(module)) + { + // something is wrong + Py_XDECREF(module); + Error(Logger::getInstance("EFFECTENGINE"), "Unable to retrieve the effect object from the Python runtime"); + return nullptr; + } + + // retrieve the capsule with the effect + PyObject * effectCapsule = PyObject_GetAttrString(module, "__effectObj"); + Py_XDECREF(module); + + if (!PyCapsule_CheckExact(effectCapsule)) + { + // something is wrong + Py_XDECREF(effectCapsule); + Error(Logger::getInstance("EFFECTENGINE"), "Unable to retrieve the effect object from the Python runtime"); + return nullptr; + } + + // Get the effect from the capsule + Effect * effect = reinterpret_cast(PyCapsule_GetPointer(effectCapsule, nullptr)); + Py_XDECREF(effectCapsule); + return effect; +} diff --git a/libsrc/flatbufserver/CMakeLists.txt b/libsrc/flatbufserver/CMakeLists.txt new file mode 100644 index 00000000..48a31dd8 --- /dev/null +++ b/libsrc/flatbufserver/CMakeLists.txt @@ -0,0 +1,42 @@ + +# 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} +) + +FILE ( GLOB FLATBUFSERVER_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) + +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_BINARY_DIR}) + ENDFOREACH(FBS_FILE) + +# let cmake know about new generated source files +set_source_files_properties( + ${Flatbuffer_GENERATED_FBS} PROPERTIES GENERATED TRUE +) + +add_library(flatbufserver + ${FLATBUFSERVER_SOURCES} + ${Flatbuffer_GENERATED_FBS} +) + +target_link_libraries(flatbufserver + hyperion-utils + flatbuffers + Qt5::Network + Qt5::Core +) diff --git a/libsrc/flatbufserver/FlatBufferClient.cpp b/libsrc/flatbufserver/FlatBufferClient.cpp new file mode 100644 index 00000000..419eb937 --- /dev/null +++ b/libsrc/flatbufserver/FlatBufferClient.cpp @@ -0,0 +1,204 @@ +#include "FlatBufferClient.h" + +// qt +#include +#include +#include +#include + +#include +FlatBufferClient::FlatBufferClient(QTcpSocket* socket, const int &timeout, QObject *parent) + : QObject(parent) + , _log(Logger::getInstance("FLATBUFSERVER")) + , _socket(socket) + , _clientAddress("@"+socket->peerAddress().toString()) + , _timeoutTimer(new QTimer(this)) + , _timeout(timeout * 1000) + , _priority() + , _hyperion(Hyperion::getInstance()) +{ + // timer setup + _timeoutTimer->setSingleShot(true); + _timeoutTimer->setInterval(_timeout); + connect(_timeoutTimer, &QTimer::timeout, this, &FlatBufferClient::forceClose); + + // connect socket signals + connect(_socket, &QTcpSocket::readyRead, this, &FlatBufferClient::readyRead); + connect(_socket, &QTcpSocket::disconnected, this, &FlatBufferClient::disconnected); +} + +void FlatBufferClient::readyRead() +{ + _timeoutTimer->start(); + + _receiveBuffer += _socket->readAll(); + + // check if we can read a header + while(_receiveBuffer.size() >= 4) + { + uint32_t messageSize = + ((_receiveBuffer[0]<<24) & 0xFF000000) | + ((_receiveBuffer[1]<<16) & 0x00FF0000) | + ((_receiveBuffer[2]<< 8) & 0x0000FF00) | + ((_receiveBuffer[3] ) & 0x000000FF); + + // check if we can read a complete message + if((uint32_t) _receiveBuffer.size() < messageSize + 4) return; + + // extract message only and remove header + msg from buffer :: QByteArray::remove() does not return the removed data + const QByteArray msg = _receiveBuffer.right(messageSize); + _receiveBuffer.remove(0, messageSize + 4); + + const auto* msgData = reinterpret_cast(msg.constData()); + flatbuffers::Verifier verifier(msgData, messageSize); + + if (hyperionnet::VerifyRequestBuffer(verifier)) + { + auto message = hyperionnet::GetRequest(msgData); + handleMessage(message); + continue; + } + sendErrorReply("Unable to parse message"); + } +} + +void FlatBufferClient::forceClose() +{ + _socket->close(); +} + +void FlatBufferClient::disconnected() +{ + Debug(_log, "Socket Closed"); + _socket->deleteLater(); + _hyperion->clear(_priority); + emit clientDisconnected(); +} + +void FlatBufferClient::handleMessage(const hyperionnet::Request * req) +{ + const void* reqPtr; + if ((reqPtr = req->command_as_Color()) != nullptr) { + handleColorCommand(static_cast(reqPtr)); + } else if ((reqPtr = req->command_as_Image()) != nullptr) { + handleImageCommand(static_cast(reqPtr)); + } else if ((reqPtr = req->command_as_Clear()) != nullptr) { + handleClearCommand(static_cast(reqPtr)); + } else if ((reqPtr = req->command_as_Register()) != nullptr) { + handleRegisterCommand(static_cast(reqPtr)); + } else { + sendErrorReply("Received invalid packet."); + } +} + +void FlatBufferClient::handleColorCommand(const hyperionnet::Color *colorReq) +{ + // extract parameters + const int32_t rgbData = colorReq->data(); + ColorRgb color; + color.red = qRed(rgbData); + color.green = qGreen(rgbData); + color.blue = qBlue(rgbData); + + // set output + _hyperion->setColor(_priority, color, colorReq->duration()); + + // send reply + sendSuccessReply(); +} + +void FlatBufferClient::handleRegisterCommand(const hyperionnet::Register *regReq) +{ + _priority = regReq->priority(); + _hyperion->registerInput(_priority, hyperion::COMP_FLATBUFSERVER, regReq->origin()->c_str()+_clientAddress); + + auto reply = hyperionnet::CreateReplyDirect(_builder, nullptr, -1, (_priority ? _priority : -1)); + _builder.Finish(reply); + + // send reply + sendMessage(); +} + +void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) +{ + // extract parameters + int duration = image->duration(); + + const void* reqPtr; + if ((reqPtr = image->data_as_RawImage()) != nullptr) + { + const auto *img = static_cast(reqPtr); + const auto & imageData = img->data(); + const int width = img->width(); + const int height = img->height(); + + if ((int) imageData->size() != width*height*3) + { + sendErrorReply("Size of image data does not match with the width and height"); + return; + } + + Image image(width, height); + memmove(image.memptr(), imageData->data(), imageData->size()); + _hyperion->setInputImage(_priority, image, duration); + } + + // send reply + sendSuccessReply(); +} + + +void FlatBufferClient::handleClearCommand(const hyperionnet::Clear *clear) +{ + // extract parameters + const int priority = clear->priority(); + + if (priority == -1) { + _hyperion->clearall(); + } + else { + // Check if we are clearing ourselves. + if (priority == _priority) { + _priority = -1; + } + + _hyperion->clear(priority); + } + + sendSuccessReply(); +} + +void FlatBufferClient::handleNotImplemented() +{ + sendErrorReply("Command not implemented"); +} + +void FlatBufferClient::sendMessage() +{ + auto size = _builder.GetSize(); + const uint8_t* buffer = _builder.GetBufferPointer(); + uint8_t sizeData[] = {uint8_t(size >> 24), uint8_t(size >> 16), uint8_t(size >> 8), uint8_t(size)}; + _socket->write((const char *) sizeData, sizeof(sizeData)); + _socket->write((const char *)buffer, size); + _socket->flush(); + _builder.Clear(); +} + +void FlatBufferClient::sendSuccessReply() +{ + auto reply = hyperionnet::CreateReplyDirect(_builder); + _builder.Finish(reply); + + // send reply + sendMessage(); +} + +void FlatBufferClient::sendErrorReply(const std::string &error) +{ + // create reply + auto reply = hyperionnet::CreateReplyDirect(_builder, error.c_str()); + _builder.Finish(reply); + + // send reply + sendMessage(); +} diff --git a/libsrc/flatbufserver/FlatBufferClient.h b/libsrc/flatbufserver/FlatBufferClient.h new file mode 100644 index 00000000..cc934b32 --- /dev/null +++ b/libsrc/flatbufserver/FlatBufferClient.h @@ -0,0 +1,134 @@ +#pragma once + +// util +#include +#include +#include +#include + +// flatbuffer FBS +#include "hyperion_reply_generated.h" +#include "hyperion_request_generated.h" + +class QTcpSocket; +class QTimer; +class Hyperion; + +namespace flatbuf { +class HyperionRequest; +} + +/// +/// @brief Socket (client) of FlatBufferServer +/// +class FlatBufferClient : public QObject +{ + Q_OBJECT +public: + /// + /// @brief Construct the client + /// @param socket The socket + /// @param timeout The timeout when a client is automatically disconnected and the priority unregistered + /// @param parent The parent + /// + explicit FlatBufferClient(QTcpSocket* socket, const int &timeout, QObject *parent = nullptr); + +signals: + /// + /// @brief forward register data to HyperionDaemon + /// + void registerGlobalInput(const int priority, const hyperion::Components& component, const QString& origin = "System", const QString& owner = "", unsigned smooth_cfg = 0); + + /// + /// @brief forward prepared image to HyperionDaemon + /// + const bool setGlobalInputImage(const int priority, const Image& image, const int timeout_ms = -1); + + /// + /// @brief Emits whenever the client disconnected + /// + void clientDisconnected(); + +public slots: + /// + /// @brief close the socket and call disconnected() + /// + void forceClose(); + +private slots: + /// + /// @brief Is called whenever the socket got new data to read + /// + void readyRead(); + + /// + /// @brief Is called when the socket closed the connection, also requests thread exit + /// + void disconnected(); + +private: + /// + /// @brief Handle the received message + /// + void handleMessage(const hyperionnet::Request * req); + + /// + /// Register new priority + /// + void handleRegisterCommand(const hyperionnet::Register *regReq); + + /// + /// @brief Hande Color message + /// + void handleColorCommand(const hyperionnet::Color *colorReq); + + /// + /// Handle an incoming Image message + /// + /// @param image the incoming image + /// + void handleImageCommand(const hyperionnet::Image *image); + + /// + /// @brief Handle clear command + /// + /// @param clear the incoming clear request + /// + void handleClearCommand(const hyperionnet::Clear *clear); + + /// + /// Send handle not implemented + /// + void handleNotImplemented(); + + /// + /// Send a message to the connected client + /// + void sendMessage(); + + /// + /// Send a standard reply indicating success + /// + void sendSuccessReply(); + + /// + /// Send an error message back to the client + /// + /// @param error String describing the error + /// + void sendErrorReply(const std::string & error); + +private: + Logger *_log; + QTcpSocket *_socket; + const QString _clientAddress; + QTimer *_timeoutTimer; + int _timeout; + int _priority; + Hyperion* _hyperion; + + QByteArray _receiveBuffer; + + // Flatbuffers builder + flatbuffers::FlatBufferBuilder _builder; +}; diff --git a/libsrc/flatbufserver/FlatBufferConnection.cpp b/libsrc/flatbufserver/FlatBufferConnection.cpp new file mode 100644 index 00000000..cfcafc46 --- /dev/null +++ b/libsrc/flatbufserver/FlatBufferConnection.cpp @@ -0,0 +1,221 @@ +// stl includes +#include + +// Qt includes +#include + +// protoserver includes +#include + +FlatBufferConnection::FlatBufferConnection(const QString& origin, const QString & address, const int& priority, const bool& skipReply) + : _socket() + , _origin(origin) + , _priority(priority) + , _prevSocketState(QAbstractSocket::UnconnectedState) + , _log(Logger::getInstance("FLATBUFCONNECTION")) + , _registered(false) +{ + QStringList parts = address.split(":"); + if (parts.size() != 2) + { + throw std::runtime_error(QString("FLATBUFCONNECTION ERROR: Unable to parse address (%1)").arg(address).toStdString()); + } + _host = parts[0]; + + bool ok; + _port = parts[1].toUShort(&ok); + if (!ok) + { + throw std::runtime_error(QString("FLATBUFCONNECTION ERROR: Unable to parse the port (%1)").arg(parts[1]).toStdString()); + } + + if(!skipReply) + connect(&_socket, &QTcpSocket::readyRead, this, &FlatBufferConnection::readData, Qt::UniqueConnection); + + // init connect + Info(_log, "Connecting to Hyperion: %s:%d", _host.toStdString().c_str(), _port); + connectToHost(); + + // start the connection timer + _timer.setInterval(5000); + + connect(&_timer, &QTimer::timeout, this, &FlatBufferConnection::connectToHost); + _timer.start(); +} + +FlatBufferConnection::~FlatBufferConnection() +{ + _timer.stop(); + _socket.close(); +} + +void FlatBufferConnection::readData() +{ + _receiveBuffer += _socket.readAll(); + + // check if we can read a header + while(_receiveBuffer.size() >= 4) + { + uint32_t messageSize = + ((_receiveBuffer[0]<<24) & 0xFF000000) | + ((_receiveBuffer[1]<<16) & 0x00FF0000) | + ((_receiveBuffer[2]<< 8) & 0x0000FF00) | + ((_receiveBuffer[3] ) & 0x000000FF); + + // check if we can read a complete message + if((uint32_t) _receiveBuffer.size() < messageSize + 4) return; + + // extract message only and remove header + msg from buffer :: QByteArray::remove() does not return the removed data + const QByteArray msg = _receiveBuffer.right(messageSize); + _receiveBuffer.remove(0, messageSize + 4); + + const uint8_t* msgData = reinterpret_cast(msg.constData()); + flatbuffers::Verifier verifier(msgData, messageSize); + + if (hyperionnet::VerifyReplyBuffer(verifier)) + { + parseReply(hyperionnet::GetReply(msgData)); + continue; + } + Error(_log, "Unable to parse reply"); + } +} + +void FlatBufferConnection::setSkipReply(const bool& skip) +{ + if(skip) + disconnect(&_socket, &QTcpSocket::readyRead, 0, 0); + else + connect(&_socket, &QTcpSocket::readyRead, this, &FlatBufferConnection::readData, Qt::UniqueConnection); +} + +void FlatBufferConnection::setRegister(const QString& origin, int priority) +{ + auto registerReq = hyperionnet::CreateRegister(_builder, _builder.CreateString(QSTRING_CSTR(origin)), priority); + auto req = hyperionnet::CreateRequest(_builder, hyperionnet::Command_Register, registerReq.Union()); + + _builder.Finish(req); + uint32_t size = _builder.GetSize(); + const uint8_t header[] = { + uint8_t((size >> 24) & 0xFF), + uint8_t((size >> 16) & 0xFF), + uint8_t((size >> 8) & 0xFF), + uint8_t((size ) & 0xFF)}; + + // write message + int count = 0; + count += _socket.write(reinterpret_cast(header), 4); + count += _socket.write(reinterpret_cast(_builder.GetBufferPointer()), size); + _socket.flush(); + _builder.Clear(); +} + +void FlatBufferConnection::setColor(const ColorRgb & color, int priority, int duration) +{ + auto colorReq = hyperionnet::CreateColor(_builder, (color.red << 16) | (color.green << 8) | color.blue, duration); + auto req = hyperionnet::CreateRequest(_builder, hyperionnet::Command_Color, colorReq.Union()); + + _builder.Finish(req); + sendMessage(_builder.GetBufferPointer(), _builder.GetSize()); +} + +void FlatBufferConnection::setImage(const Image &image) +{ + auto imgData = _builder.CreateVector(reinterpret_cast(image.memptr()), image.size()); + auto rawImg = hyperionnet::CreateRawImage(_builder, imgData, image.width(), image.height()); + auto imageReq = hyperionnet::CreateImage(_builder, hyperionnet::ImageType_RawImage, rawImg.Union(), -1); + auto req = hyperionnet::CreateRequest(_builder,hyperionnet::Command_Image,imageReq.Union()); + + _builder.Finish(req); + sendMessage(_builder.GetBufferPointer(), _builder.GetSize()); +} + +void FlatBufferConnection::clear(int priority) +{ + auto clearReq = hyperionnet::CreateClear(_builder, priority); + auto req = hyperionnet::CreateRequest(_builder,hyperionnet::Command_Clear, clearReq.Union()); + + _builder.Finish(req); + sendMessage(_builder.GetBufferPointer(), _builder.GetSize()); +} + +void FlatBufferConnection::clearAll() +{ + clear(-1); +} + +void FlatBufferConnection::connectToHost() +{ + // try connection only when + if (_socket.state() == QAbstractSocket::UnconnectedState) + _socket.connectToHost(_host, _port); +} + +void FlatBufferConnection::sendMessage(const uint8_t* buffer, uint32_t size) +{ + // print out connection message only when state is changed + if (_socket.state() != _prevSocketState ) + { + _registered = false; + switch (_socket.state() ) + { + case QAbstractSocket::UnconnectedState: + Info(_log, "No connection to Hyperion: %s:%d", _host.toStdString().c_str(), _port); + break; + case QAbstractSocket::ConnectedState: + Info(_log, "Connected to Hyperion: %s:%d", _host.toStdString().c_str(), _port); + break; + default: + Debug(_log, "Connecting to Hyperion: %s:%d", _host.toStdString().c_str(), _port); + break; + } + _prevSocketState = _socket.state(); + } + + + if (_socket.state() != QAbstractSocket::ConnectedState) + return; + + if(!_registered) + { + setRegister(_origin, _priority); + return; + } + + const uint8_t header[] = { + uint8_t((size >> 24) & 0xFF), + uint8_t((size >> 16) & 0xFF), + uint8_t((size >> 8) & 0xFF), + uint8_t((size ) & 0xFF)}; + + // write message + int count = 0; + count += _socket.write(reinterpret_cast(header), 4); + count += _socket.write(reinterpret_cast(buffer), size); + _socket.flush(); + _builder.Clear(); +} + +bool FlatBufferConnection::parseReply(const hyperionnet::Reply *reply) +{ + if (!reply->error()) + { + // no error set must be a success or registered or video + const auto videoMode = reply->video(); + const auto registered = reply->registered(); + if (videoMode != -1) { + // We got a video reply. + emit setVideoMode(static_cast(videoMode)); + return true; + } + + // We got a registered reply. + if (registered != -1 && registered != _priority) + _registered = false; + else + _registered = true; + + return true; + } + return false; +} diff --git a/libsrc/flatbufserver/FlatBufferServer.cpp b/libsrc/flatbufserver/FlatBufferServer.cpp new file mode 100644 index 00000000..ab87eb3e --- /dev/null +++ b/libsrc/flatbufserver/FlatBufferServer.cpp @@ -0,0 +1,106 @@ +#include +#include "FlatBufferClient.h" + +// qt +#include +#include +#include + +FlatBufferServer::FlatBufferServer(const QJsonDocument& config, QObject* parent) + : QObject(parent) + , _server(new QTcpServer(this)) + , _log(Logger::getInstance("FLATBUFSERVER")) + , _timeout(5000) + , _config(config) +{ + +} + +FlatBufferServer::~FlatBufferServer() +{ + stopServer(); + delete _server; +} + +void FlatBufferServer::initServer() +{ + connect(_server, &QTcpServer::newConnection, this, &FlatBufferServer::newConnection); + + // apply config + handleSettingsUpdate(settings::FLATBUFSERVER, _config); +} + +void FlatBufferServer::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::FLATBUFSERVER) + { + const QJsonObject& obj = config.object(); + + quint16 port = obj["port"].toInt(19400); + + // port check + if(_server->serverPort() != port) + { + stopServer(); + _port = port; + } + + // new timeout just for new connections + _timeout = obj["timeout"].toInt(5000); + // enable check + obj["enable"].toBool(true) ? startServer() : stopServer(); + } +} + +void FlatBufferServer::newConnection() +{ + while(_server->hasPendingConnections()) + { + if(QTcpSocket* socket = _server->nextPendingConnection()) + { + Debug(_log, "New connection from %s", QSTRING_CSTR(socket->peerAddress().toString())); + FlatBufferClient *client = new FlatBufferClient(socket, _timeout, this); + // internal + connect(client, &FlatBufferClient::clientDisconnected, this, &FlatBufferServer::clientDisconnected); + // forward data + //connect(clientThread, &FlatBufferClient::); + _openConnections.append(client); + } + } +} + +void FlatBufferServer::clientDisconnected() +{ + FlatBufferClient* client = qobject_cast(sender()); + client->deleteLater(); + _openConnections.removeAll(client); +} + +void FlatBufferServer::startServer() +{ + if(!_server->isListening()) + { + if(!_server->listen(QHostAddress::Any, _port)) + { + Error(_log,"Failed to bind port %d", _port); + } + else + { + Info(_log,"Started on port %d", _port); + } + } +} + +void FlatBufferServer::stopServer() +{ + if(_server->isListening()) + { + // close client connections + for(const auto& client : _openConnections) + { + client->forceClose(); + } + _server->close(); + Info(_log, "Stopped"); + } +} diff --git a/libsrc/flatbufserver/hyperion_reply.fbs b/libsrc/flatbufserver/hyperion_reply.fbs new file mode 100644 index 00000000..c80d1b61 --- /dev/null +++ b/libsrc/flatbufserver/hyperion_reply.fbs @@ -0,0 +1,9 @@ +namespace hyperionnet; + +table Reply { + error:string; + video:int = -1; + registered:int = -1; +} + +root_type Reply; diff --git a/libsrc/flatbufserver/hyperion_request.fbs b/libsrc/flatbufserver/hyperion_request.fbs new file mode 100644 index 00000000..1c6e3d49 --- /dev/null +++ b/libsrc/flatbufserver/hyperion_request.fbs @@ -0,0 +1,37 @@ +namespace hyperionnet; + +// A priority value of -1 clears all priorities +table Register { + origin:string (required); + priority:int; +} + +table RawImage { + data:[ubyte]; + width:int = -1; + height:int = -1; +} + +union ImageType {RawImage} + +table Image { + data:ImageType (required); + duration:int = -1; +} + +table Clear { + priority:int; +} + +table Color { + data:int = -1; + duration:int = -1; +} + +union Command {Color, Image, Clear, Register} + +table Request { + command:Command (required); +} + +root_type Request; diff --git a/libsrc/grabber/CMakeLists.txt b/libsrc/grabber/CMakeLists.txt index a075e79d..6163f5b3 100644 --- a/libsrc/grabber/CMakeLists.txt +++ b/libsrc/grabber/CMakeLists.txt @@ -21,3 +21,7 @@ endif (ENABLE_V4L2) if (ENABLE_X11) add_subdirectory(x11) endif() + +if (ENABLE_QT) + add_subdirectory(qt) +endif() diff --git a/libsrc/grabber/amlogic/AmlogicWrapper.cpp b/libsrc/grabber/amlogic/AmlogicWrapper.cpp index 8db02591..205787e8 100644 --- a/libsrc/grabber/amlogic/AmlogicWrapper.cpp +++ b/libsrc/grabber/amlogic/AmlogicWrapper.cpp @@ -1,7 +1,7 @@ #include -AmlogicWrapper::AmlogicWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority) - : GrabberWrapper("AmLogic", &_grabber, grabWidth, grabHeight, updateRate_Hz, priority, hyperion::COMP_GRABBER) +AmlogicWrapper::AmlogicWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz) + : GrabberWrapper("AmLogic", &_grabber, grabWidth, grabHeight, updateRate_Hz) , _grabber(grabWidth, grabHeight) {} diff --git a/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp b/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp index ec7aacc3..54bcbf52 100644 --- a/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp +++ b/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp @@ -7,7 +7,7 @@ #include "grabber/DispmanxFrameGrabber.h" DispmanxFrameGrabber::DispmanxFrameGrabber(const unsigned width, const unsigned height) - : Grabber("DISPMANXGRABBER", width, height) + : Grabber("DISPMANXGRABBER", 0, 0) , _vc_display(0) , _vc_resource(0) , _vc_flags(0) @@ -20,48 +20,70 @@ DispmanxFrameGrabber::DispmanxFrameGrabber(const unsigned width, const unsigned // Initiase BCM bcm_host_init(); - { - // Check if the display can be opened and display the current resolution - // Open the connection to the display - _vc_display = vc_dispmanx_display_open(0); - assert(_vc_display > 0); + // Check if the display can be opened and display the current resolution + // Open the connection to the display + _vc_display = vc_dispmanx_display_open(0); + assert(_vc_display > 0); - // Obtain the display information - DISPMANX_MODEINFO_T vc_info; - int result = vc_dispmanx_display_get_info(_vc_display, &vc_info); - // Keep compiler happy in 'release' mode - (void)result; - assert(result == 0); + // Obtain the display information + DISPMANX_MODEINFO_T vc_info; + int result = vc_dispmanx_display_get_info(_vc_display, &vc_info); + // Keep compiler happy in 'release' mode + (void)result; + + // Close the display + vc_dispmanx_display_close(_vc_display); + + if(result != 0) + { + Error(_log, "Failed to open display! Probably no permissions to access the capture interface"); + setEnabled(false); + return; + } + else Info(_log, "Display opened with resolution: %dx%d", vc_info.width, vc_info.height); - // Close the displaye - vc_dispmanx_display_close(_vc_display); - } - - // Create the resources for capturing image - uint32_t vc_nativeImageHandle; - _vc_resource = vc_dispmanx_resource_create( - VC_IMAGE_RGBA32, - width, - height, - &vc_nativeImageHandle); - assert(_vc_resource); - - // Define the capture rectangle with the same size - vc_dispmanx_rect_set(&_rectangle, 0, 0, width, height); + // init the resource and capture rectangle + setWidthHeight(width, height); } DispmanxFrameGrabber::~DispmanxFrameGrabber() { - delete[] _captureBuffer; - - // Clean up resources - vc_dispmanx_resource_delete(_vc_resource); + freeResources(); // De-init BCM bcm_host_deinit(); } +void DispmanxFrameGrabber::freeResources() +{ + delete[] _captureBuffer; + // Clean up resources + vc_dispmanx_resource_delete(_vc_resource); +} + +bool DispmanxFrameGrabber::setWidthHeight(int width, int height) +{ + if(Grabber::setWidthHeight(width, height)) + { + if(_vc_resource != 0) + vc_dispmanx_resource_delete(_vc_resource); + // Create the resources for capturing image + uint32_t vc_nativeImageHandle; + _vc_resource = vc_dispmanx_resource_create( + VC_IMAGE_RGBA32, + width, + height, + &vc_nativeImageHandle); + assert(_vc_resource); + + // Define the capture rectangle with the same size + vc_dispmanx_rect_set(&_rectangle, 0, 0, width, height); + return true; + } + return false; +} + void DispmanxFrameGrabber::setFlags(const int vc_flags) { _vc_flags = vc_flags; @@ -143,8 +165,8 @@ int DispmanxFrameGrabber::grabFrame(Image & image) unsigned capturePitch = (_rectangle.width * sizeof(ColorRgba) + 63) & (~63); // grab to temp buffer if image pitch isn't valid or if we are cropping - if (imagePitch != capturePitch - || (unsigned)_rectangle.width != imageWidth + if (imagePitch != capturePitch + || (unsigned)_rectangle.width != imageWidth || (unsigned)_rectangle.height != imageHeight) { // check if we need to resize the capture buffer diff --git a/libsrc/grabber/dispmanx/DispmanxWrapper.cpp b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp index 633fa60d..0acf81d8 100644 --- a/libsrc/grabber/dispmanx/DispmanxWrapper.cpp +++ b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp @@ -1,10 +1,10 @@ #include -DispmanxWrapper::DispmanxWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority) - : GrabberWrapper("Dispmanx", &_grabber, grabWidth, grabHeight, updateRate_Hz, priority, hyperion::COMP_GRABBER) +DispmanxWrapper::DispmanxWrapper(const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz) + : GrabberWrapper("Dispmanx", &_grabber, grabWidth, grabHeight, updateRate_Hz) , _grabber(grabWidth, grabHeight) { - setImageProcessorEnabled(false); + } void DispmanxWrapper::action() diff --git a/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp b/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp index 3eec042c..9cf2675b 100755 --- a/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp +++ b/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp @@ -16,31 +16,9 @@ FramebufferFrameGrabber::FramebufferFrameGrabber(const QString & device, const u : Grabber("FRAMEBUFFERGRABBER", width, height) , _fbfd(0) , _fbp(0) - , _fbDevice(device) + , _fbDevice() { - int result; - struct fb_var_screeninfo vinfo; - - // Check if the framebuffer device can be opened and display the current resolution - _fbfd = open(QSTRING_CSTR(_fbDevice), O_RDONLY); - if (_fbfd == 0) - { - Error(_log, "Error openning %s", QSTRING_CSTR(_fbDevice)); - } - else - { - // get variable screen information - result = ioctl (_fbfd, FBIOGET_VSCREENINFO, &vinfo); - if (result != 0) - { - Error(_log, "Could not get screen information"); - } - else - { - Info(_log, "Display opened with resolution: %dx%d@%dbit", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel); - } - close(_fbfd); - } + setDevicePath(device); } FramebufferFrameGrabber::~FramebufferFrameGrabber() @@ -63,7 +41,7 @@ int FramebufferFrameGrabber::grabFrame(Image & image) bytesPerPixel = vinfo.bits_per_pixel / 8; capSize = vinfo.xres * vinfo.yres * bytesPerPixel; - + switch (vinfo.bits_per_pixel) { case 16: pixelFormat = PIXELFORMAT_BGR16; break; @@ -76,7 +54,7 @@ int FramebufferFrameGrabber::grabFrame(Image & image) } /* map the device to memory */ - _fbp = (unsigned char*)mmap(0, capSize, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, _fbfd, 0); + _fbp = (unsigned char*)mmap(0, capSize, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, _fbfd, 0); _imageResampler.setHorizontalPixelDecimation(vinfo.xres/_width); _imageResampler.setVerticalPixelDecimation(vinfo.yres/_height); @@ -86,9 +64,41 @@ int FramebufferFrameGrabber::grabFrame(Image & image) vinfo.xres * bytesPerPixel, pixelFormat, image); - + munmap(_fbp, capSize); close(_fbfd); return 0; } + +void FramebufferFrameGrabber::setDevicePath(const QString& path) +{ + if(_fbDevice != path) + { + _fbDevice = path; + int result; + struct fb_var_screeninfo vinfo; + + // Check if the framebuffer device can be opened and display the current resolution + _fbfd = open(QSTRING_CSTR(_fbDevice), O_RDONLY); + if (_fbfd == 0) + { + Error(_log, "Error openning %s", QSTRING_CSTR(_fbDevice)); + } + else + { + // get variable screen information + result = ioctl (_fbfd, FBIOGET_VSCREENINFO, &vinfo); + if (result != 0) + { + Error(_log, "Could not get screen information"); + } + else + { + Info(_log, "Display opened with resolution: %dx%d@%dbit", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel); + } + close(_fbfd); + } + + } +} diff --git a/libsrc/grabber/framebuffer/FramebufferWrapper.cpp b/libsrc/grabber/framebuffer/FramebufferWrapper.cpp index 9a7cc367..4a88bbee 100644 --- a/libsrc/grabber/framebuffer/FramebufferWrapper.cpp +++ b/libsrc/grabber/framebuffer/FramebufferWrapper.cpp @@ -1,7 +1,7 @@ #include -FramebufferWrapper::FramebufferWrapper(const QString & device, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority) - : GrabberWrapper("FrameBuffer", &_grabber, grabWidth, grabHeight, updateRate_Hz, priority, hyperion::COMP_GRABBER) +FramebufferWrapper::FramebufferWrapper(const QString & device, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz) + : GrabberWrapper("FrameBuffer", &_grabber, grabWidth, grabHeight, updateRate_Hz) , _grabber(device, grabWidth, grabHeight) {} diff --git a/libsrc/grabber/osx/OsxFrameGrabber.cpp b/libsrc/grabber/osx/OsxFrameGrabber.cpp index 97862f0f..7306b40a 100755 --- a/libsrc/grabber/osx/OsxFrameGrabber.cpp +++ b/libsrc/grabber/osx/OsxFrameGrabber.cpp @@ -7,30 +7,10 @@ OsxFrameGrabber::OsxFrameGrabber(const unsigned display, const unsigned width, const unsigned height) : Grabber("OSXGRABBER", width, height) - , _screenIndex(display) + , _screenIndex(100) { - CGImageRef image; - CGDisplayCount displayCount; - CGDirectDisplayID displays[8]; - - // get list of displays - CGGetActiveDisplayList(8, displays, &displayCount); - if (_screenIndex + 1 > displayCount) - { - Error(_log, "Display with index %d is not available. Using main display", _screenIndex); - _display = kCGDirectMainDisplay; - } - else - { - _display = displays[_screenIndex]; - } - - image = CGDisplayCreateImage(_display); - assert(image != NULL); - - Info(_log, "Display opened with resolution: %dx%d@%dbit", CGImageGetWidth(image), CGImageGetHeight(image), CGImageGetBitsPerPixel(image)); - - CGImageRelease(image); + // check if display is available + setDisplayIndex(display); } OsxFrameGrabber::~OsxFrameGrabber() @@ -43,11 +23,11 @@ int OsxFrameGrabber::grabFrame(Image & image) CGImageRef dispImage; CFDataRef imgData; - unsigned char * pImgData; + unsigned char * pImgData; unsigned dspWidth, dspHeight; - + dispImage = CGDisplayCreateImage(_display); - + // display lost, use main if (dispImage == NULL && _display) { @@ -63,7 +43,7 @@ int OsxFrameGrabber::grabFrame(Image & image) pImgData = (unsigned char*) CFDataGetBytePtr(imgData); dspWidth = CGImageGetWidth(dispImage); dspHeight = CGImageGetHeight(dispImage); - + _imageResampler.setHorizontalPixelDecimation(dspWidth/_width); _imageResampler.setVerticalPixelDecimation(dspHeight/_height); _imageResampler.processImage( pImgData, @@ -72,9 +52,47 @@ int OsxFrameGrabber::grabFrame(Image & image) CGImageGetBytesPerRow(dispImage), PIXELFORMAT_BGR32, image); - + CFRelease(imgData); CGImageRelease(dispImage); return 0; } + +void OsxFrameGrabber::setDisplayIndex(int index) +{ + if(_screenIndex != index) + { + _screenIndex = index; + + CGImageRef image; + CGDisplayCount displayCount; + CGDirectDisplayID displays[8]; + + // get list of displays + CGGetActiveDisplayList(8, displays, &displayCount); + if (_screenIndex + 1 > displayCount) + { + Error(_log, "Display with index %d is not available. Using main display", _screenIndex); + _display = kCGDirectMainDisplay; + } + else + { + _display = displays[_screenIndex]; + } + + image = CGDisplayCreateImage(_display); + if(image == NULL) + { + Error(_log, "Failed to open main display, disable capture interface"); + setEnabled(false); + return; + } + else + setEnabled(true); + + Info(_log, "Display opened with resolution: %dx%d@%dbit", CGImageGetWidth(image), CGImageGetHeight(image), CGImageGetBitsPerPixel(image)); + + CGImageRelease(image); + } +} diff --git a/libsrc/grabber/osx/OsxWrapper.cpp b/libsrc/grabber/osx/OsxWrapper.cpp index f543eada..6324532c 100644 --- a/libsrc/grabber/osx/OsxWrapper.cpp +++ b/libsrc/grabber/osx/OsxWrapper.cpp @@ -1,7 +1,7 @@ #include -OsxWrapper::OsxWrapper(const unsigned display, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz, const int priority) - : GrabberWrapper("OSX FrameGrabber", &_grabber, grabWidth, grabHeight, updateRate_Hz, priority, hyperion::COMP_GRABBER) +OsxWrapper::OsxWrapper(const unsigned display, const unsigned grabWidth, const unsigned grabHeight, const unsigned updateRate_Hz) + : GrabberWrapper("OSX FrameGrabber", &_grabber, grabWidth, grabHeight, updateRate_Hz) , _grabber(display, grabWidth, grabHeight) {} diff --git a/libsrc/grabber/qt/CMakeLists.txt b/libsrc/grabber/qt/CMakeLists.txt new file mode 100644 index 00000000..1ac244e3 --- /dev/null +++ b/libsrc/grabber/qt/CMakeLists.txt @@ -0,0 +1,16 @@ +# Define the current source locations +SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/qt) + +find_package(Qt5Widgets REQUIRED) + +include_directories( ${X11_INCLUDES} ) + +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} ) + +target_link_libraries(qt-grabber + hyperion + Qt5::Widgets +) diff --git a/libsrc/grabber/qt/QtGrabber.cpp b/libsrc/grabber/qt/QtGrabber.cpp new file mode 100644 index 00000000..7e5b7d83 --- /dev/null +++ b/libsrc/grabber/qt/QtGrabber.cpp @@ -0,0 +1,199 @@ +// proj +#include + +// qt +#include +#include +#include +#include +#include + +QtGrabber::QtGrabber(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation, int display) + : Grabber("QTGRABBER", 0, 0, cropLeft, cropRight, cropTop, cropBottom) + , _display(unsigned(display)) + , _pixelDecimation(pixelDecimation) + , _screenWidth(0) + , _screenHeight(0) + , _src_x(0) + , _src_y(0) + , _src_x_max(0) + , _src_y_max(0) + , _screen(nullptr) +{ + _useImageResampler = false; + + // init + setupDisplay(); +} + +QtGrabber::~QtGrabber() +{ + freeResources(); +} + +void QtGrabber::freeResources() +{ + // cleanup + if (_screen != nullptr) + { + delete _screen; + _screen = nullptr; + } +} + +const bool QtGrabber::setupDisplay() +{ + // cleanup last screen + freeResources(); + + QScreen* primary = QGuiApplication::primaryScreen(); + QList screens = QGuiApplication::screens(); + // inject main screen at 0, if not nullptr + if(primary != nullptr) + { + screens.prepend(primary); + // remove last main screen if twice in list + if(screens.lastIndexOf(primary) > 0) + screens.removeAt(screens.lastIndexOf(primary)); + } + + if(screens.isEmpty()) + { + Error(_log, "No displays found to capture from!"); + return false; + } + + Info(_log,"Available Displays:"); + int index = 0; + for(auto screen : screens) + { + const QRect geo = screen->geometry(); + Info(_log,"Display %d: Name:%s Geometry: (L,T,R,B) %d,%d,%d,%d Depth:%dbit", index, QSTRING_CSTR(screen->name()), geo.left(), geo.top() ,geo.right(), geo.bottom(), screen->depth()); + index++; + } + + // be sure the index is available + if(_display > unsigned(screens.size()-1)) + { + Info(_log, "The requested display index '%d' is not available, falling back to display 0", _display); + _display = 0; + } + + // init the requested display + _screen = screens.at(_display); + connect(_screen, &QScreen::geometryChanged, this, &QtGrabber::geometryChanged); + updateScreenDimensions(true); + + Info(_log,"Initialized display %d", _display); + return true; +} + +void QtGrabber::geometryChanged(const QRect &geo) +{ + Info(_log, "The current display changed geometry to (L,T,R,B) %d,%d,%d,%d", geo.left(), geo.top() ,geo.right(), geo.bottom()); + updateScreenDimensions(true); +} + +int QtGrabber::grabFrame(Image & image) +{ + if(_screen == nullptr) + { + // reinit, this will disable capture on failure + setEnabled(setupDisplay()); + return -1; + } + QPixmap originalPixmap = _screen->grabWindow(0, _src_x, _src_y, _src_x_max, _src_y_max); + QPixmap resizedPixmap = originalPixmap.scaled(_width,_height); + QImage img = resizedPixmap.toImage().convertToFormat( QImage::Format_RGB888); + memcpy(image.memptr(), img.bits(),_width*_height*3); + + return 0; +} + +int QtGrabber::updateScreenDimensions(const bool& force) +{ + if(!_screen) + return -1; + + const QRect& geo = _screen->geometry(); + if (!force && _screenWidth == unsigned(geo.right()) && _screenHeight == unsigned(geo.bottom())) + { + // No update required + return 0; + } + + Info(_log, "Update of screen resolution: [%dx%d] to [%dx%d]", _screenWidth, _screenHeight, geo.right(), geo.bottom()); + _screenWidth = geo.right() - geo.left(); + _screenHeight = geo.bottom() - geo.top(); + + int width=0, height=0; + + // Image scaling is performed by Qt + width = (_screenWidth > unsigned(_cropLeft + _cropRight)) + ? ((_screenWidth - _cropLeft - _cropRight) / _pixelDecimation) + : (_screenWidth / _pixelDecimation); + + height = (_screenHeight > unsigned(_cropTop + _cropBottom)) + ? ((_screenHeight - _cropTop - _cropBottom) / _pixelDecimation) + : (_screenHeight / _pixelDecimation); + + + // calculate final image dimensions and adjust top/left cropping in 3D modes + switch (_videoMode) + { + case VIDEO_3DSBS: + _width = width /2; + _height = height; + _src_x = _cropLeft / 2; + _src_y = _cropTop; + _src_x_max = (_screenWidth / 2) - _cropRight; + _src_y_max = _screenHeight - _cropBottom; + break; + case VIDEO_3DTAB: + _width = width; + _height = height / 2; + _src_x = _cropLeft; + _src_y = _cropTop / 2; + _src_x_max = _screenWidth - _cropRight; + _src_y_max = (_screenHeight / 2) - _cropBottom; + break; + case VIDEO_2D: + default: + _width = width; + _height = height; + _src_x = _cropLeft; + _src_y = _cropTop; + _src_x_max = _screenWidth - _cropRight; + _src_y_max = _screenHeight - _cropBottom; + break; + } + + Info(_log, "Update output image resolution to [%dx%d]", _width, _height); + return 1; +} + +void QtGrabber::setVideoMode(VideoMode mode) +{ + Grabber::setVideoMode(mode); + updateScreenDimensions(true); +} + +void QtGrabber::setPixelDecimation(int pixelDecimation) +{ + _pixelDecimation = pixelDecimation; +} + +void QtGrabber::setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom) +{ + Grabber::setCropping(cropLeft, cropRight, cropTop, cropBottom); + updateScreenDimensions(true); +} + +void QtGrabber::setDisplayIndex(int index) +{ + if(_display != unsigned(index)) + { + _display = unsigned(index); + setupDisplay(); + } +} diff --git a/libsrc/grabber/qt/QtWrapper.cpp b/libsrc/grabber/qt/QtWrapper.cpp new file mode 100644 index 00000000..6326711c --- /dev/null +++ b/libsrc/grabber/qt/QtWrapper.cpp @@ -0,0 +1,11 @@ +#include + +QtWrapper::QtWrapper(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation, int display, const unsigned updateRate_Hz) + : GrabberWrapper("Qt", &_grabber, 0, 0, updateRate_Hz) + , _grabber(cropLeft, cropRight, cropTop, cropBottom, pixelDecimation, display) +{} + +void QtWrapper::action() +{ + transferFrame(_grabber); +} diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index 16df0b17..2f17f238 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -24,27 +24,22 @@ #define CLEAR(x) memset(&(x), 0, sizeof(x)) V4L2Grabber::V4L2Grabber(const QString & device - , int input , VideoStandard videoStandard , PixelFormat pixelFormat - , unsigned width - , unsigned height - , int frameDecimation - , int horizontalPixelDecimation - , int verticalPixelDecimation + , int pixelDecimation ) - : Grabber("V4L2:"+device, width, height) - , _deviceName(device) - , _input(input) + : Grabber("V4L2:"+device) + , _deviceName() + , _input(-1) , _videoStandard(videoStandard) , _ioMethod(IO_METHOD_MMAP) , _fileDescriptor(-1) , _buffers() , _pixelFormat(pixelFormat) + , _pixelDecimation(-1) , _lineLength(-1) , _frameByteSize(-1) - , _frameDecimation(qMax(1, frameDecimation)) - , _noSignalCounterThreshold(50) + , _noSignalCounterThreshold(40) , _noSignalThresholdColor(ColorRgb{0,0,0}) , _signalDetectionEnabled(true) , _noSignalDetected(false) @@ -53,16 +48,15 @@ V4L2Grabber::V4L2Grabber(const QString & device , _y_frac_min(0.25) , _x_frac_max(0.75) , _y_frac_max(0.75) - , _currentFrame(0) , _streamNotifier(nullptr) , _initialized(false) , _deviceAutoDiscoverEnabled(false) - { - _imageResampler.setHorizontalPixelDecimation(qMax(1, horizontalPixelDecimation)); - _imageResampler.setVerticalPixelDecimation(qMax(1, verticalPixelDecimation)); - + setPixelDecimation(pixelDecimation); getV4Ldevices(); + + // init + setDeviceVideoStandard(device, videoStandard); } V4L2Grabber::~V4L2Grabber() @@ -72,10 +66,11 @@ V4L2Grabber::~V4L2Grabber() void V4L2Grabber::uninit() { - Debug(_log,"uninit grabber: %s", QSTRING_CSTR(_deviceName)); // stop if the grabber was not stopped if (_initialized) { + Debug(_log,"uninit grabber: %s", QSTRING_CSTR(_deviceName)); + stop(); uninit_device(); close_device(); @@ -83,7 +78,6 @@ void V4L2Grabber::uninit() } } - bool V4L2Grabber::init() { if (! _initialized) @@ -98,7 +92,8 @@ bool V4L2Grabber::init() { v4lDevices_str += "\t"+ dev.first + "\t" + dev.second + "\n"; } - Info(_log, "available V4L2 devices:\n%s", QSTRING_CSTR(v4lDevices_str)); + if (!v4lDevices_str.isEmpty()) + Info(_log, "available V4L2 devices:\n%s", QSTRING_CSTR(v4lDevices_str)); } if ( _deviceName == "auto" ) @@ -138,10 +133,14 @@ bool V4L2Grabber::init() bool opened = false; try { - open_device(); - opened = true; - init_device(_videoStandard, _input); - _initialized = true; + // do not init with unknown device + if(_deviceName != "unknown") + { + open_device(); + opened = true; + init_device(_videoStandard, _input); + _initialized = true; + } } catch(std::exception& e) { @@ -245,11 +244,13 @@ void V4L2Grabber::open_device() if (-1 == stat(QSTRING_CSTR(_deviceName), &st)) { throw_errno_exception("Cannot identify '" + _deviceName + "'"); + return; } if (!S_ISCHR(st.st_mode)) { throw_exception("'" + _deviceName + "' is no device"); + return; } _fileDescriptor = open(QSTRING_CSTR(_deviceName), O_RDWR | O_NONBLOCK, 0); @@ -257,6 +258,7 @@ void V4L2Grabber::open_device() if (-1 == _fileDescriptor) { throw_errno_exception("Cannot open '" + _deviceName + "'"); + return; } // create the notifier for when a new frame is available @@ -268,7 +270,10 @@ void V4L2Grabber::open_device() void V4L2Grabber::close_device() { if (-1 == close(_fileDescriptor)) + { throw_errno_exception("close"); + return; + } _fileDescriptor = -1; @@ -288,6 +293,7 @@ void V4L2Grabber::init_read(unsigned int buffer_size) if (!_buffers[0].start) { throw_exception("Out of memory"); + return; } } @@ -304,13 +310,16 @@ void V4L2Grabber::init_mmap() if (-1 == xioctl(VIDIOC_REQBUFS, &req)) { if (EINVAL == errno) { throw_exception("'" + _deviceName + "' does not support memory mapping"); + return; } else { throw_errno_exception("VIDIOC_REQBUFS"); + return; } } if (req.count < 2) { throw_exception("Insufficient buffer memory on " + _deviceName); + return; } _buffers.resize(req.count); @@ -325,7 +334,10 @@ void V4L2Grabber::init_mmap() buf.index = n_buffers; if (-1 == xioctl(VIDIOC_QUERYBUF, &buf)) + { throw_errno_exception("VIDIOC_QUERYBUF"); + return; + } _buffers[n_buffers].length = buf.length; _buffers[n_buffers].start = @@ -336,7 +348,10 @@ void V4L2Grabber::init_mmap() _fileDescriptor, buf.m.offset); if (MAP_FAILED == _buffers[n_buffers].start) + { throw_errno_exception("mmap"); + return; + } } } @@ -354,8 +369,10 @@ void V4L2Grabber::init_userp(unsigned int buffer_size) if (EINVAL == errno) { throw_exception("'" + _deviceName + "' does not support user pointer"); + return; } else { throw_errno_exception("VIDIOC_REQBUFS"); + return; } } @@ -367,6 +384,7 @@ void V4L2Grabber::init_userp(unsigned int buffer_size) if (!_buffers[n_buffers].start) { throw_exception("Out of memory"); + return; } } } @@ -378,14 +396,17 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) { if (EINVAL == errno) { throw_exception("'" + _deviceName + "' is no V4L2 device"); + return; } else { throw_errno_exception("VIDIOC_QUERYCAP"); + return; } } if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { throw_exception("'" + _deviceName + "' is no video capture device"); + return; } switch (_ioMethod) { @@ -393,6 +414,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (!(cap.capabilities & V4L2_CAP_READWRITE)) { throw_exception("'" + _deviceName + "' does not support read i/o"); + return; } break; @@ -401,6 +423,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (!(cap.capabilities & V4L2_CAP_STREAMING)) { throw_exception("'" + _deviceName + "' does not support streaming i/o"); + return; } break; } @@ -438,6 +461,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (-1 == xioctl(VIDIOC_S_INPUT, &input)) { throw_errno_exception("VIDIOC_S_INPUT"); + return; } } @@ -450,6 +474,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (-1 == xioctl(VIDIOC_S_STD, &std_id)) { throw_errno_exception("VIDIOC_S_STD"); + return; } } break; @@ -459,6 +484,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (-1 == xioctl(VIDIOC_S_STD, &std_id)) { throw_errno_exception("VIDIOC_S_STD"); + return; } } break; @@ -468,6 +494,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (-1 == xioctl(VIDIOC_S_STD, &std_id)) { throw_errno_exception("VIDIOC_S_STD"); + return; } } break; @@ -485,6 +512,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (-1 == xioctl(VIDIOC_G_FMT, &fmt)) { throw_errno_exception("VIDIOC_G_FMT"); + return; } // set the requested pixel format @@ -505,27 +533,18 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) break; } - // set the requested withd and height - if (_width > 0 || _height > 0) - { - if (_width > 0) - { - fmt.fmt.pix.width = _width; - } +// TODO Does never accept own sizes? use always _imageResampler instead +/* - if (fmt.fmt.pix.height > 0) - { - fmt.fmt.pix.height = _height; - } - } - - // set the line length - _lineLength = fmt.fmt.pix.bytesperline; + // calc the size based on pixelDecimation + fmt.fmt.pix.width = fmt.fmt.pix.width / _pixelDecimation; + fmt.fmt.pix.height = fmt.fmt.pix.height / _pixelDecimation; // set the settings if (-1 == xioctl(VIDIOC_S_FMT, &fmt)) { throw_errno_exception("VIDIOC_S_FMT"); + return; } // get the format settings again @@ -533,7 +552,11 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) if (-1 == xioctl(VIDIOC_G_FMT, &fmt)) { throw_errno_exception("VIDIOC_G_FMT"); + return; } +*/ + // set the line length + _lineLength = fmt.fmt.pix.bytesperline; // store width & height _width = fmt.fmt.pix.width; @@ -563,6 +586,7 @@ void V4L2Grabber::init_device(VideoStandard videoStandard, int input) break; default: throw_exception("Only pixel formats UYVY, YUYV, and RGB32 are supported"); + return; } switch (_ioMethod) { @@ -590,7 +614,10 @@ void V4L2Grabber::uninit_device() case IO_METHOD_MMAP: for (size_t i = 0; i < _buffers.size(); ++i) if (-1 == munmap(_buffers[i].start, _buffers[i].length)) + { throw_errno_exception("munmap"); + return; + } break; case IO_METHOD_USERPTR: @@ -620,11 +647,17 @@ void V4L2Grabber::start_capturing() buf.index = i; if (-1 == xioctl(VIDIOC_QBUF, &buf)) + { throw_errno_exception("VIDIOC_QBUF"); + return; + } } v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == xioctl(VIDIOC_STREAMON, &type)) + { throw_errno_exception("VIDIOC_STREAMON"); + return; + } break; } case IO_METHOD_USERPTR: @@ -640,11 +673,17 @@ void V4L2Grabber::start_capturing() buf.length = _buffers[i].length; if (-1 == xioctl(VIDIOC_QBUF, &buf)) + { throw_errno_exception("VIDIOC_QBUF"); + return; + } } v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == xioctl(VIDIOC_STREAMON, &type)) + { throw_errno_exception("VIDIOC_STREAMON"); + return; + } break; } } @@ -692,6 +731,7 @@ int V4L2Grabber::read_frame() default: throw_errno_exception("read"); + return 0; } } @@ -718,6 +758,7 @@ int V4L2Grabber::read_frame() default: throw_errno_exception("VIDIOC_DQBUF"); + return 0; } } @@ -728,6 +769,7 @@ int V4L2Grabber::read_frame() if (-1 == xioctl(VIDIOC_QBUF, &buf)) { throw_errno_exception("VIDIOC_QBUF"); + return 0; } break; @@ -752,6 +794,7 @@ int V4L2Grabber::read_frame() default: throw_errno_exception("VIDIOC_DQBUF"); + return 0; } } @@ -768,6 +811,7 @@ int V4L2Grabber::read_frame() if (-1 == xioctl(VIDIOC_QBUF, &buf)) { throw_errno_exception("VIDIOC_QBUF"); + return 0; } break; } @@ -783,19 +827,15 @@ int V4L2Grabber::read_frame() bool V4L2Grabber::process_image(const void *p, int size) { - if (++_currentFrame >= _frameDecimation) + // We do want a new frame... + if (size != _frameByteSize) { - // We do want a new frame... - if (size != _frameByteSize) - { - Error(_log, "Frame too small: %d != %d", size, _frameByteSize); - } - else - { - process_image(reinterpret_cast(p)); - _currentFrame = 0; // restart counting - return true; - } + Error(_log, "Frame too small: %d != %d", size, _frameByteSize); + } + else + { + process_image(reinterpret_cast(p)); + return true; } return false; @@ -874,20 +914,52 @@ int V4L2Grabber::xioctl(int request, void *arg) void V4L2Grabber::throw_exception(const QString & error) { - throw std::runtime_error(error.toStdString()); + Error(_log, "Throws error: %s", QSTRING_CSTR(error)); } void V4L2Grabber::throw_errno_exception(const QString & error) { - throw std::runtime_error(QString(error + " error code " + QString::number(errno) + ", " + strerror(errno)).toStdString()); + Error(_log, "Throws error nr: %s", QSTRING_CSTR(QString(error + " error code " + QString::number(errno) + ", " + strerror(errno)))); } void V4L2Grabber::setSignalDetectionEnable(bool enable) { - _signalDetectionEnabled = enable; + if(_signalDetectionEnabled != enable) + { + _signalDetectionEnabled = enable; + Info(_log, "Signal detection is now %s", enable ? "enabled" : "disabled"); + } } bool V4L2Grabber::getSignalDetectionEnabled() { return _signalDetectionEnabled; } + +void V4L2Grabber::setPixelDecimation(int pixelDecimation) +{ + if(_pixelDecimation != pixelDecimation) + { + _pixelDecimation = pixelDecimation; + _imageResampler.setHorizontalPixelDecimation(pixelDecimation); + _imageResampler.setVerticalPixelDecimation(pixelDecimation); + } +} + +void V4L2Grabber::setDeviceVideoStandard(QString device, VideoStandard videoStandard) +{ + if(_deviceName != device || _videoStandard != videoStandard) + { + // extract input of device + QChar input = device.at(device.size() - 1); + _input = input.isNumber() ? input.digitValue() : -1; + + uninit(); + _deviceName = device; + _videoStandard = videoStandard; + + // start if init is a success + if(init()) + start(); + } +} diff --git a/libsrc/grabber/v4l2/V4L2Wrapper.cpp b/libsrc/grabber/v4l2/V4L2Wrapper.cpp index aef95648..f6b161f5 100644 --- a/libsrc/grabber/v4l2/V4L2Wrapper.cpp +++ b/libsrc/grabber/v4l2/V4L2Wrapper.cpp @@ -2,45 +2,27 @@ #include -#include +// qt +#include V4L2Wrapper::V4L2Wrapper(const QString &device, - int input, VideoStandard videoStandard, PixelFormat pixelFormat, - unsigned width, - unsigned height, - int frameDecimation, - int pixelDecimation, - double redSignalThreshold, - double greenSignalThreshold, - double blueSignalThreshold, - const int priority) - : GrabberWrapper("V4L2:"+device, &_grabber, width, height, 8, priority, hyperion::COMP_V4L) + int pixelDecimation ) + : GrabberWrapper("V4L2:"+device, &_grabber, 0, 0, 10) , _grabber(device, - input, videoStandard, pixelFormat, - width, - height, - frameDecimation, - pixelDecimation, pixelDecimation) { - // set the signal detection threshold of the grabber - _grabber.setSignalThreshold( redSignalThreshold, greenSignalThreshold, blueSignalThreshold, 50); _ggrabber = &_grabber; // register the image type qRegisterMetaType>("Image"); - qRegisterMetaType>("std::vector"); - qRegisterMetaType("hyperion::Components"); // Handle the image in the captured thread using a direct connection QObject::connect(&_grabber, SIGNAL(newFrame(Image)), this, SLOT(newFrame(Image)), Qt::DirectConnection); QObject::connect(&_grabber, SIGNAL(readError(const char*)), this, SLOT(readError(const char*)), Qt::DirectConnection); - - _timer.setInterval(500); } bool V4L2Wrapper::start() @@ -54,6 +36,11 @@ void V4L2Wrapper::stop() GrabberWrapper::stop(); } +void V4L2Wrapper::setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold) +{ + _grabber.setSignalThreshold( redSignalThreshold, greenSignalThreshold, blueSignalThreshold, 50); +} + void V4L2Wrapper::setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom) { _grabber.setCropping(cropLeft, cropRight, cropTop, cropBottom); @@ -66,11 +53,7 @@ void V4L2Wrapper::setSignalDetectionOffset(double verticalMin, double horizontal void V4L2Wrapper::newFrame(const Image &image) { - emit emitImage(_priority, image, _timeout_ms); - - // process the new image - _processor->process(image, _ledColors); - setColors(_ledColors, _timeout_ms); + emit systemImage(image); } void V4L2Wrapper::readError(const char* err) @@ -79,21 +62,9 @@ void V4L2Wrapper::readError(const char* err) stop(); } -void V4L2Wrapper::checkSources() -{ - if ( _hyperion->isCurrentPriority(_priority)) - { - _grabber.start(); - } - else - { - _grabber.stop(); - } -} - void V4L2Wrapper::action() { - checkSources(); + // dummy as v4l get notifications from stream } void V4L2Wrapper::setSignalDetectionEnable(bool enable) diff --git a/libsrc/grabber/x11/X11Grabber.cpp b/libsrc/grabber/x11/X11Grabber.cpp index 988ae4e2..4a3e3754 100755 --- a/libsrc/grabber/x11/X11Grabber.cpp +++ b/libsrc/grabber/x11/X11Grabber.cpp @@ -1,17 +1,15 @@ #include #include -X11Grabber::X11Grabber(bool useXGetImage, int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation) +X11Grabber::X11Grabber(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation) : Grabber("X11GRABBER", 0, 0, cropLeft, cropRight, cropTop, cropBottom) - , _useXGetImage(useXGetImage) , _x11Display(nullptr) , _pixmap(None) , _srcFormat(nullptr) , _dstFormat(nullptr) , _srcPicture(None) , _dstPicture(None) - , _horizontalDecimation(horizontalPixelDecimation) - , _verticalDecimation(verticalPixelDecimation) + , _pixelDecimation(pixelDecimation) , _screenWidth(0) , _screenHeight(0) , _src_x(cropLeft) @@ -37,13 +35,13 @@ void X11Grabber::freeResources() { // Cleanup allocated resources of the X11 grab XDestroyImage(_xImage); - if(_XShmAvailable && !_useXGetImage) + if(_XShmAvailable) { XShmDetach(_x11Display, &_shminfo); shmdt(_shminfo.shmaddr); shmctl(_shminfo.shmid, IPC_RMID, 0); } - if (_XRenderAvailable && !_useXGetImage) + if (_XRenderAvailable) { XRenderFreePicture(_x11Display, _srcPicture); XRenderFreePicture(_x11Display, _dstPicture); @@ -53,7 +51,7 @@ void X11Grabber::freeResources() void X11Grabber::setupResources() { - if(_XShmAvailable && !_useXGetImage) + if(_XShmAvailable) { _xImage = XShmCreateImage(_x11Display, _windowAttr.visual, _windowAttr.depth, ZPixmap, NULL, &_shminfo, _width, _height); _shminfo.shmid = shmget(IPC_PRIVATE, _xImage->bytes_per_line * _xImage->height, IPC_CREAT|0777); @@ -62,7 +60,7 @@ void X11Grabber::setupResources() _shminfo.readOnly = False; XShmAttach(_x11Display, &_shminfo); } - if (_XRenderAvailable && !_useXGetImage) + if (_XRenderAvailable) { if(_XShmPixmapAvailable) { @@ -96,22 +94,23 @@ bool X11Grabber::Setup() } return false; } - + _window = DefaultRootWindow(_x11Display); int dummy, pixmaps_supported; - + _XRenderAvailable = XRenderQueryExtension(_x11Display, &dummy, &dummy); _XShmAvailable = XShmQueryExtension(_x11Display); XShmQueryVersion(_x11Display, &dummy, &dummy, &pixmaps_supported); _XShmPixmapAvailable = pixmaps_supported && XShmPixmapFormat(_x11Display) == ZPixmap; - + // Image scaling is performed by XRender when available, otherwise by ImageResampler - _imageResampler.setHorizontalPixelDecimation(_XRenderAvailable ? 1 : _horizontalDecimation); - _imageResampler.setVerticalPixelDecimation(_XRenderAvailable ? 1 : _verticalDecimation); + _imageResampler.setHorizontalPixelDecimation(_XRenderAvailable ? 1 : _pixelDecimation); + _imageResampler.setVerticalPixelDecimation(_XRenderAvailable ? 1 : _pixelDecimation); bool result = (updateScreenDimensions(true) >=0); ErrorIf(!result, _log, "X11 Grabber start failed"); + setEnabled(result); return result; } @@ -121,13 +120,13 @@ int X11Grabber::grabFrame(Image & image, bool forceUpdate) if (forceUpdate) updateScreenDimensions(forceUpdate); - - if (_XRenderAvailable && !_useXGetImage) + + if (_XRenderAvailable) { - double scale_x = static_cast(_windowAttr.width / _horizontalDecimation) / static_cast(_windowAttr.width); - double scale_y = static_cast(_windowAttr.height / _verticalDecimation) / static_cast(_windowAttr.height); + double scale_x = static_cast(_windowAttr.width / _pixelDecimation) / static_cast(_windowAttr.width); + double scale_y = static_cast(_windowAttr.height / _pixelDecimation) / static_cast(_windowAttr.height); double scale = qMin(scale_y, scale_x); - + _transform = { { @@ -148,27 +147,27 @@ int X11Grabber::grabFrame(Image & image, bool forceUpdate) } } }; - + XRenderSetPictureTransform (_x11Display, _srcPicture, &_transform); - + // display, op, src, mask, dest, src_x = cropLeft, - // src_y = cropTop, mask_x, mask_y, dest_x, dest_y, width, height + // src_y = cropTop, mask_x, mask_y, dest_x, dest_y, width, height XRenderComposite( - _x11Display, PictOpSrc, _srcPicture, None, _dstPicture, ( _src_x/_horizontalDecimation), - (_src_y/_verticalDecimation), 0, 0, 0, 0, _width, _height); - + _x11Display, PictOpSrc, _srcPicture, None, _dstPicture, ( _src_x/_pixelDecimation), + (_src_y/_pixelDecimation), 0, 0, 0, 0, _width, _height); + XSync(_x11Display, False); - + if (_XShmAvailable) { XShmGetImage(_x11Display, _pixmap, _xImage, 0, 0, AllPlanes); } else { - _xImage = XGetImage(_x11Display, _pixmap, 0, 0, _width, _height, AllPlanes, ZPixmap); + _xImage = XGetImage(_x11Display, _pixmap, 0, 0, _width, _height, AllPlanes, ZPixmap); } } - else if (_XShmAvailable && !_useXGetImage) + else if (_XShmAvailable) { // use xshm XShmGetImage(_x11Display, _window, _xImage, _src_x, _src_y, AllPlanes); @@ -213,20 +212,20 @@ int X11Grabber::updateScreenDimensions(bool force) Info(_log, "Update of screen resolution: [%dx%d] to [%dx%d]", _screenWidth, _screenHeight, _windowAttr.width, _windowAttr.height); _screenWidth = _windowAttr.width; _screenHeight = _windowAttr.height; - + int width=0, height=0; - + // Image scaling is performed by XRender when available, otherwise by ImageResampler - if (_XRenderAvailable && !_useXGetImage) + if (_XRenderAvailable) { width = (_screenWidth > unsigned(_cropLeft + _cropRight)) - ? ((_screenWidth - _cropLeft - _cropRight) / _horizontalDecimation) - : _screenWidth / _horizontalDecimation; - + ? ((_screenWidth - _cropLeft - _cropRight) / _pixelDecimation) + : _screenWidth / _pixelDecimation; + height = (_screenHeight > unsigned(_cropTop + _cropBottom)) - ? ((_screenHeight - _cropTop - _cropBottom) / _verticalDecimation) - : _screenHeight / _verticalDecimation; - + ? ((_screenHeight - _cropTop - _cropBottom) / _pixelDecimation) + : _screenHeight / _pixelDecimation; + Info(_log, "Using XRender for grabbing"); } else @@ -234,11 +233,11 @@ int X11Grabber::updateScreenDimensions(bool force) width = (_screenWidth > unsigned(_cropLeft + _cropRight)) ? (_screenWidth - _cropLeft - _cropRight) : _screenWidth; - + height = (_screenHeight > unsigned(_cropTop + _cropBottom)) ? (_screenHeight - _cropTop - _cropBottom) : _screenHeight; - + Info(_log, "Using XGetImage for grabbing"); } @@ -265,7 +264,7 @@ int X11Grabber::updateScreenDimensions(bool force) _src_y = _cropTop; break; } - + Info(_log, "Update output image resolution: [%dx%d] to [%dx%d]", _image.width(), _image.height(), _width, _height); _image.resize(_width, _height); @@ -276,7 +275,21 @@ int X11Grabber::updateScreenDimensions(bool force) void X11Grabber::setVideoMode(VideoMode mode) { - Info(_log, "a %d", mode); Grabber::setVideoMode(mode); updateScreenDimensions(true); } + +void X11Grabber::setPixelDecimation(int pixelDecimation) +{ + if(_pixelDecimation != pixelDecimation) + { + _pixelDecimation = pixelDecimation; + updateScreenDimensions(true); + } +} + +void X11Grabber::setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom) +{ + Grabber::setCropping(cropLeft, cropRight, cropTop, cropBottom); + if(_x11Display != nullptr) updateScreenDimensions(true); // segfault on init +} diff --git a/libsrc/grabber/x11/X11Wrapper.cpp b/libsrc/grabber/x11/X11Wrapper.cpp index 75f5f117..ee445ea2 100644 --- a/libsrc/grabber/x11/X11Wrapper.cpp +++ b/libsrc/grabber/x11/X11Wrapper.cpp @@ -1,8 +1,8 @@ #include -X11Wrapper::X11Wrapper(bool useXGetImage, int cropLeft, int cropRight, int cropTop, int cropBottom, int horizontalPixelDecimation, int verticalPixelDecimation, const unsigned updateRate_Hz, const int priority) - : GrabberWrapper("X11", &_grabber, 0, 0, updateRate_Hz, priority, hyperion::COMP_GRABBER) - , _grabber(useXGetImage, cropLeft, cropRight, cropTop, cropBottom, horizontalPixelDecimation, verticalPixelDecimation) +X11Wrapper::X11Wrapper(int cropLeft, int cropRight, int cropTop, int cropBottom, int pixelDecimation, const unsigned updateRate_Hz) + : GrabberWrapper("X11", &_grabber, 0, 0, updateRate_Hz) + , _grabber(cropLeft, cropRight, cropTop, cropBottom, pixelDecimation) , _init(false) {} diff --git a/libsrc/hyperion/CMakeLists.txt b/libsrc/hyperion/CMakeLists.txt index 428d7738..eb96a92d 100644 --- a/libsrc/hyperion/CMakeLists.txt +++ b/libsrc/hyperion/CMakeLists.txt @@ -3,6 +3,10 @@ SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/hyperion) SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/hyperion) +include_directories( + ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver +) + FILE ( GLOB Hyperion_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) SET(Hyperion_RESOURCES ${CURRENT_SOURCE_DIR}/resource.qrc) @@ -15,8 +19,11 @@ add_library(hyperion target_link_libraries(hyperion blackborder hyperion-utils + flatbufserver + flatbuffers leddevice bonjour + boblightserver effectengine ${QT_LIBRARIES} ) diff --git a/libsrc/hyperion/CaptureCont.cpp b/libsrc/hyperion/CaptureCont.cpp new file mode 100644 index 00000000..12c5cd32 --- /dev/null +++ b/libsrc/hyperion/CaptureCont.cpp @@ -0,0 +1,138 @@ +#include + +// hyperion includes +#include + +// utils includes +#include + +// qt includes +#include + +CaptureCont::CaptureCont(Hyperion* hyperion) + : QObject() + , _hyperion(hyperion) + , _systemCaptEnabled(false) + , _systemInactiveTimer(new QTimer(this)) + , _v4lCaptEnabled(false) + , _v4lInactiveTimer(new QTimer(this)) +{ + // settings changes + connect(_hyperion, &Hyperion::settingsChanged, this, &CaptureCont::handleSettingsUpdate); + + // comp changes + connect(_hyperion, &Hyperion::componentStateChanged, this, &CaptureCont::componentStateChanged); + + // inactive timer system + connect(_systemInactiveTimer, &QTimer::timeout, this, &CaptureCont::setSystemInactive); + _systemInactiveTimer->setSingleShot(true); + _systemInactiveTimer->setInterval(5000); + + // inactive timer v4l + connect(_v4lInactiveTimer, &QTimer::timeout, this, &CaptureCont::setV4lInactive); + _v4lInactiveTimer->setSingleShot(true); + _v4lInactiveTimer->setInterval(1000); + + // init + handleSettingsUpdate(settings::INSTCAPTURE, _hyperion->getSetting(settings::INSTCAPTURE)); +} + +CaptureCont::~CaptureCont() +{ +} + +void CaptureCont::handleV4lImage(const Image & image) +{ + _v4lInactiveTimer->start(); + _hyperion->setInputImage(_v4lCaptPrio, image); +} + +void CaptureCont::handleSystemImage(const Image& image) +{ + _systemInactiveTimer->start(); + _hyperion->setInputImage(_systemCaptPrio, image); +} + +void CaptureCont::setSystemCaptureEnable(const bool& enable) +{ + if(_systemCaptEnabled != enable) + { + if(enable) + { + _hyperion->registerInput(_systemCaptPrio, hyperion::COMP_GRABBER); + connect(GlobalSignals::getInstance(), &GlobalSignals::setSystemImage, this, &CaptureCont::handleSystemImage); + connect(GlobalSignals::getInstance(), &GlobalSignals::setSystemImage, _hyperion, &Hyperion::forwardProtoMessage); + } + else + { + disconnect(GlobalSignals::getInstance(), &GlobalSignals::setSystemImage, 0, 0); + _hyperion->clear(_systemCaptPrio); + } + _systemCaptEnabled = enable; + _hyperion->getComponentRegister().componentStateChanged(hyperion::COMP_GRABBER, enable); + } +} + +void CaptureCont::setV4LCaptureEnable(const bool& enable) +{ + if(_v4lCaptEnabled != enable) + { + if(enable) + { + _hyperion->registerInput(_v4lCaptPrio, hyperion::COMP_V4L); + connect(GlobalSignals::getInstance(), &GlobalSignals::setV4lImage, this, &CaptureCont::handleV4lImage); + connect(GlobalSignals::getInstance(), &GlobalSignals::setSystemImage, _hyperion, &Hyperion::forwardProtoMessage); + } + else + { + disconnect(GlobalSignals::getInstance(), &GlobalSignals::setV4lImage, 0, 0); + _hyperion->clear(_v4lCaptPrio); + _v4lInactiveTimer->stop(); + } + _v4lCaptEnabled = enable; + _hyperion->getComponentRegister().componentStateChanged(hyperion::COMP_V4L, enable); + } +} + +void CaptureCont::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::INSTCAPTURE) + { + const QJsonObject& obj = config.object(); + if(_v4lCaptPrio != obj["v4lPriority"].toInt(240)) + { + setV4LCaptureEnable(false); // clear prio + _v4lCaptPrio = obj["v4lPriority"].toInt(240); + } + if(_systemCaptPrio != obj["systemPriority"].toInt(250)) + { + setSystemCaptureEnable(false); // clear prio + _systemCaptPrio = obj["systemPriority"].toInt(250); + } + + setV4LCaptureEnable(obj["v4lEnable"].toBool(true)); + setSystemCaptureEnable(obj["systemEnable"].toBool(true)); + } +} + +void CaptureCont::componentStateChanged(const hyperion::Components component, bool enable) +{ + if(component == hyperion::COMP_GRABBER) + { + setSystemCaptureEnable(enable); + } + else if(component == hyperion::COMP_V4L) + { + setV4LCaptureEnable(enable); + } +} + +void CaptureCont::setV4lInactive() +{ + _hyperion->setInputInactive(_v4lCaptPrio); +} + +void CaptureCont::setSystemInactive() +{ + _hyperion->setInputInactive(_systemCaptPrio); +} diff --git a/libsrc/hyperion/ComponentRegister.cpp b/libsrc/hyperion/ComponentRegister.cpp index 41f52c75..4a686aeb 100644 --- a/libsrc/hyperion/ComponentRegister.cpp +++ b/libsrc/hyperion/ComponentRegister.cpp @@ -1,27 +1,72 @@ #include #include -ComponentRegister::ComponentRegister() - : _log(Logger::getInstance("ComponentRegister")) +#include + +using namespace hyperion; + +ComponentRegister::ComponentRegister(Hyperion* hyperion) + : _hyperion(hyperion) + , _log(Logger::getInstance("ComponentRegister")) { + // init all comps to false + QVector vect; + vect << COMP_ALL << COMP_SMOOTHING << COMP_BLACKBORDER << COMP_FORWARDER << COMP_UDPLISTENER << COMP_BOBLIGHTSERVER << COMP_GRABBER << COMP_V4L << COMP_LEDDEVICE; + for(auto e : vect) + { + _componentStates.emplace(e, ((e == COMP_ALL) ? true : false)); + } } ComponentRegister::~ComponentRegister() { } +bool ComponentRegister::setHyperionEnable(const bool& state) +{ + if(!state && _prevComponentStates.empty()) + { + Debug(_log,"Disable Hyperion, store current component states"); + for(const auto comp : _componentStates) + { + // save state + _prevComponentStates.emplace(comp.first, comp.second); + // disable if enabled + if(comp.second) + _hyperion->setComponentState(comp.first, false); + } + componentStateChanged(COMP_ALL, false); + return true; + } + else if(state && !_prevComponentStates.empty()) + { + Debug(_log,"Enable Hyperion, recover previous component states"); + for(const auto comp : _prevComponentStates) + { + // if comp was enabled, enable again + if(comp.second) + _hyperion->setComponentState(comp.first, true); + + } + _prevComponentStates.clear(); + componentStateChanged(COMP_ALL, true); + return true; + } + return false; +} + +int ComponentRegister::isComponentEnabled(const hyperion::Components& comp) const +{ + return (_componentStates.count(comp)) ? _componentStates.at(comp) : -1; +} void ComponentRegister::componentStateChanged(const hyperion::Components comp, const bool activated) { - Info(_log, "%s: %s", componentToString(comp), (activated? "activated" : "off")); - _componentStates.emplace(comp,activated); - _componentStates[comp] = activated; - -/* for(auto comp : _componentStates) + if(_componentStates[comp] != activated) { - std::cout << hyperion::componentToIdString(comp.first) << " " << comp.second << std::endl; + Debug( _log, "%s: %s", componentToString(comp), (activated? "enabled" : "disabled")); + _componentStates[comp] = activated; + // emit component has changed state + emit updatedComponentState(comp, activated); } - std::cout << "\n"; - */ } - diff --git a/libsrc/hyperion/Grabber.cpp b/libsrc/hyperion/Grabber.cpp index bf0edd1b..e04a5770 100644 --- a/libsrc/hyperion/Grabber.cpp +++ b/libsrc/hyperion/Grabber.cpp @@ -13,7 +13,6 @@ Grabber::Grabber(QString grabberName, int width, int height, int cropLeft, int c , _cropBottom(0) , _enabled(true) , _log(Logger::getInstance(grabberName)) - { setVideoMode(VIDEO_2D); setCropping(cropLeft, cropRight, cropTop, cropBottom); @@ -25,12 +24,13 @@ Grabber::~Grabber() void Grabber::setEnabled(bool enable) { + Info(_log,"Capture interface is now %s", enable ? "enabled" : "disabled"); _enabled = enable; } void Grabber::setVideoMode(VideoMode mode) { - Debug(_log,"setvideomode %d", mode); + Debug(_log,"Set videomode to %d", mode); _videoMode = mode; if ( _useImageResampler ) { @@ -44,7 +44,7 @@ void Grabber::setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTo { if (cropLeft + cropRight >= (unsigned)_width || cropTop + cropBottom >= (unsigned)_height) { - Error(_log, "Rejecting invalid crop values: left: %d, right: %d, top: %d, bottom: %d", cropLeft, cropRight, cropTop, cropBottom); + Error(_log, "Rejecting invalid crop values: left: %d, right: %d, top: %d, bottom: %d, higher than height/width %d/%d", cropLeft, cropRight, cropTop, cropBottom, _height, _width); return; } } @@ -68,3 +68,21 @@ void Grabber::setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTo Info(_log, "Cropping image: width=%d height=%d; crop: left=%d right=%d top=%d bottom=%d ", _width, _height, cropLeft, cropRight, cropTop, cropBottom); } } + +bool Grabber::setWidthHeight(int width, int height) +{ + // eval changes with crop + if ( (width>0 && height>0) && (_width != width || _height != height) ) + { + if (_cropLeft + _cropRight >= width || _cropTop + _cropBottom >= height) + { + Error(_log, "Rejecting invalid width/height values as it collides with image cropping: width: %d, height: %d", width, height); + return false; + } + Debug(_log, "Set new width: %d, height: %d for capture", width, height); + _width = width; + _height = height; + return true; + } + return false; +} diff --git a/libsrc/hyperion/GrabberWrapper.cpp b/libsrc/hyperion/GrabberWrapper.cpp index 9af13779..41827b04 100644 --- a/libsrc/hyperion/GrabberWrapper.cpp +++ b/libsrc/hyperion/GrabberWrapper.cpp @@ -1,111 +1,52 @@ // Hyperion includes -#include -#include #include #include #include -GrabberWrapper::GrabberWrapper(QString grabberName, Grabber * ggrabber, unsigned width, unsigned height, const unsigned updateRate_Hz, const int priority, hyperion::Components grabberComponentId) +// utils includes +#include + +// qt +#include + +GrabberWrapper::GrabberWrapper(QString grabberName, Grabber * ggrabber, unsigned width, unsigned height, const unsigned updateRate_Hz) : _grabberName(grabberName) - , _hyperion(Hyperion::getInstance()) - , _priority(priority) - , _timer() + , _timer(new QTimer(this)) , _updateInterval_ms(1000/updateRate_Hz) - , _timeout_ms(2 * _updateInterval_ms) , _log(Logger::getInstance(grabberName)) - , _forward(true) - , _processor(ImageProcessorFactory::getInstance().newImageProcessor()) - , _grabberComponentId(grabberComponentId) , _ggrabber(ggrabber) , _image(0,0) - , _ledColors(Hyperion::getInstance()->getLedCount(), ColorRgb{0,0,0}) - , _imageProcessorEnabled(true) { - _timer.setSingleShot(false); // Configure the timer to generate events every n milliseconds - _timer.setInterval(_updateInterval_ms); + _timer->setInterval(_updateInterval_ms); _image.resize(width, height); - _processor->setSize(width, height); - _forward = _hyperion->getForwarder()->protoForwardingEnabled(); - _hyperion->getComponentRegister().componentStateChanged(hyperion::COMP_BLACKBORDER, _processor->blackBorderDetectorEnabled()); - qRegisterMetaType("hyperion::Components"); - - connect(_hyperion, SIGNAL(imageToLedsMappingChanged(int)), _processor, SLOT(setLedMappingType(int))); - connect(_hyperion, SIGNAL(componentStateChanged(hyperion::Components,bool)), this, SLOT(componentStateChanged(hyperion::Components,bool))); - connect(_hyperion, SIGNAL(videoMode(VideoMode)), this, SLOT(setVideoMode(VideoMode))); - connect(this, SIGNAL(emitImage(int, const Image&, const int)), _hyperion, SLOT(setImage(int, const Image&, const int)) ); - connect(&_timer, SIGNAL(timeout()), this, SLOT(actionWrapper())); + connect(_timer, &QTimer::timeout, this, &GrabberWrapper::action); + // connect the image forwarding + _grabberName.startsWith("V4L") + ? connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setV4lImage) + : connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setSystemImage); } GrabberWrapper::~GrabberWrapper() { stop(); Debug(_log,"Close grabber: %s", QSTRING_CSTR(_grabberName)); - delete _processor; } bool GrabberWrapper::start() { // Start the timer with the pre configured interval - _timer.start(); - _hyperion->registerPriority(_grabberName, _priority); - return _timer.isActive(); - + _timer->start(); + return _timer->isActive(); } void GrabberWrapper::stop() { // Stop the timer, effectivly stopping the process - _timer.stop(); - _hyperion->unRegisterPriority(_grabberName); -} - -void GrabberWrapper::actionWrapper() -{ - _ggrabber->setEnabled(_hyperion->isCurrentPriority(_priority)); - action(); -} - -void GrabberWrapper::componentStateChanged(const hyperion::Components component, bool enable) -{ - if (component == _grabberComponentId) - { - if (_timer.isActive() != enable) - { - if (enable) start(); - else stop(); - - _forward = _hyperion->getForwarder()->protoForwardingEnabled(); - - if ( enable == _timer.isActive() ) - { - Info(_log, "grabber change state to %s", (_timer.isActive() ? "enabled" : "disabled") ); - } - else - { - WarningIf( enable, _log, "enable grabber failed"); - } - } - _hyperion->getComponentRegister().componentStateChanged(component, _timer.isActive()); - } - - if (component == hyperion::COMP_BLACKBORDER) - { - if (_processor->blackBorderDetectorEnabled() != enable) - { - _processor->enableBlackBorderDetector(enable); - Info(_log, "bb detector change state to %s", (_processor->blackBorderDetectorEnabled() ? "enabled" : "disabled") ); - } - _hyperion->getComponentRegister().componentStateChanged(component, _processor->blackBorderDetectorEnabled()); - } -} - -void GrabberWrapper::setColors(const std::vector &ledColors, const int timeout_ms) -{ - _hyperion->setColors(_priority, ledColors, timeout_ms, true, _grabberComponentId); + _timer->stop(); } QStringList GrabberWrapper::availableGrabbers() @@ -136,11 +77,15 @@ QStringList GrabberWrapper::availableGrabbers() grabbers << "x11"; #endif + #ifdef ENABLE_QT + grabbers << "qt"; + #endif + return grabbers; } -void GrabberWrapper::setVideoMode(const VideoMode mode) +void GrabberWrapper::setVideoMode(const VideoMode& mode) { if (_ggrabber != nullptr) { @@ -154,7 +99,78 @@ void GrabberWrapper::setCropping(unsigned cropLeft, unsigned cropRight, unsigned _ggrabber->setCropping(cropLeft, cropRight, cropTop, cropBottom); } -void GrabberWrapper::setImageProcessorEnabled(bool enable) +void GrabberWrapper::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) { - _imageProcessorEnabled = enable; + if(type == settings::V4L2 || type == settings::SYSTEMCAPTURE) + { + // extract settings + QJsonObject obj; + if(config.isArray() && !config.isEmpty()) + obj = config.array().at(0).toObject(); + else + obj = config.object(); + + if(type == settings::SYSTEMCAPTURE && !_grabberName.startsWith("V4L")) + { + // width/height + _ggrabber->setWidthHeight(obj["width"].toInt(96), obj["height"].toInt(96)); + + // display index for MAC + _ggrabber->setDisplayIndex(obj["display"].toInt(0)); + + // device path for Framebuffer + _ggrabber->setDevicePath(obj["device"].toString("/dev/fb0")); + + // pixel decimation for x11 + _ggrabber->setPixelDecimation(obj["pixelDecimation"].toInt(8)); + + // crop for system capture + _ggrabber->setCropping( + obj["cropLeft"].toInt(0), + obj["cropRight"].toInt(0), + obj["cropTop"].toInt(0), + obj["cropBottom"].toInt(0)); + + // eval new update timer (not for v4l) + if(_updateInterval_ms != 1000/obj["frequency_Hz"].toInt(10)) + { + _updateInterval_ms = 1000/obj["frequency_Hz"].toInt(10); + const bool& timerWasActive = _timer->isActive(); + _timer->stop(); + _timer->setInterval(_updateInterval_ms); + if(timerWasActive) + _timer->start(); + } + } + + // v4l instances only! + if(type == settings::V4L2 && _grabberName.startsWith("V4L")) + { + // pixel decimation for v4l + _ggrabber->setPixelDecimation(obj["sizeDecimation"].toInt(8)); + + // crop for v4l + _ggrabber->setCropping( + obj["cropLeft"].toInt(0), + obj["cropRight"].toInt(0), + obj["cropTop"].toInt(0), + obj["cropBottom"].toInt(0)); + + _ggrabber->setSignalDetectionEnable(obj["signalDetection"].toBool(true)); + _ggrabber->setSignalDetectionOffset( + obj["sDHOffsetMin"].toDouble(0.25), + obj["sDVOffsetMin"].toDouble(0.25), + obj["sDHOffsetMax"].toDouble(0.75), + obj["sDVOffsetMax"].toDouble(0.75)); + _ggrabber->setSignalThreshold( + obj["redSignalThreshold"].toDouble(0.0)/100.0, + obj["greenSignalThreshold"].toDouble(0.0)/100.0, + obj["blueSignalThreshold"].toDouble(0.0)/100.0); + _ggrabber->setDeviceVideoStandard( + obj["device"].toString("auto"), + parseVideoStandard(obj["standard"].toString("no-change"))); + + } + } + } diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index 32dc9f0d..c04b2b28 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -11,35 +11,51 @@ #include #include #include +#include #include #include #include // hyperion include #include -#include +#include #include #include -// Leddevice includes -#include -#include +// utils +#include -#include "MultiColorAdjustment.h" +// Leddevice includes +#include + +#include #include "LinearColorSmoothing.h" // effect engine includes #include -#define CORE_LOGGER Logger::getInstance("Core") +// Hyperion Daemon +#include <../src/hyperiond/hyperiond.h> + +// settingsManagaer +#include + +// BGEffectHandler +#include + +// CaptureControl (Daemon capture) +#include + +// Boblight +#include Hyperion* Hyperion::_hyperion = nullptr; -Hyperion* Hyperion::initInstance(const QJsonObject& qjsonConfig, const QString configFile, const QString rootPath) +Hyperion* Hyperion::initInstance( HyperionDaemon* daemon, const quint8& instance, const QString configFile, const QString rootPath) { if ( Hyperion::_hyperion != nullptr ) throw std::runtime_error("Hyperion::initInstance can be called only one time"); - Hyperion::_hyperion = new Hyperion(qjsonConfig, configFile, rootPath); + Hyperion::_hyperion = new Hyperion(daemon, instance, configFile, rootPath); return Hyperion::_hyperion; } @@ -52,453 +68,99 @@ Hyperion* Hyperion::getInstance() return Hyperion::_hyperion; } -ColorOrder Hyperion::createColorOrder(const QJsonObject &deviceConfig) -{ - return stringToColorOrder(deviceConfig["colorOrder"].toString("rgb")); -} - -ColorAdjustment * Hyperion::createColorAdjustment(const QJsonObject & adjustmentConfig) -{ - const QString id = adjustmentConfig["id"].toString("default"); - - RgbChannelAdjustment * blackAdjustment = createRgbChannelAdjustment(adjustmentConfig, "black" , 0, 0, 0); - RgbChannelAdjustment * whiteAdjustment = createRgbChannelAdjustment(adjustmentConfig, "white" , 255,255,255); - RgbChannelAdjustment * redAdjustment = createRgbChannelAdjustment(adjustmentConfig, "red" , 255, 0, 0); - RgbChannelAdjustment * greenAdjustment = createRgbChannelAdjustment(adjustmentConfig, "green" , 0,255, 0); - RgbChannelAdjustment * blueAdjustment = createRgbChannelAdjustment(adjustmentConfig, "blue" , 0, 0,255); - RgbChannelAdjustment * cyanAdjustment = createRgbChannelAdjustment(adjustmentConfig, "cyan" , 0,255,255); - RgbChannelAdjustment * magentaAdjustment = createRgbChannelAdjustment(adjustmentConfig, "magenta", 255, 0,255); - RgbChannelAdjustment * yellowAdjustment = createRgbChannelAdjustment(adjustmentConfig, "yellow" , 255,255, 0); - RgbTransform * rgbTransform = createRgbTransform(adjustmentConfig); - - ColorAdjustment * adjustment = new ColorAdjustment(); - adjustment->_id = id; - adjustment->_rgbBlackAdjustment = *blackAdjustment; - adjustment->_rgbWhiteAdjustment = *whiteAdjustment; - adjustment->_rgbRedAdjustment = *redAdjustment; - adjustment->_rgbGreenAdjustment = *greenAdjustment; - adjustment->_rgbBlueAdjustment = *blueAdjustment; - adjustment->_rgbCyanAdjustment = *cyanAdjustment; - adjustment->_rgbMagentaAdjustment = *magentaAdjustment; - adjustment->_rgbYellowAdjustment = *yellowAdjustment; - adjustment->_rgbTransform = *rgbTransform; - - // Cleanup the allocated individual adjustments - delete blackAdjustment; - delete whiteAdjustment; - delete redAdjustment; - delete greenAdjustment; - delete blueAdjustment; - delete cyanAdjustment; - delete magentaAdjustment; - delete yellowAdjustment; - delete rgbTransform; - - return adjustment; -} - - -MultiColorAdjustment * Hyperion::createLedColorsAdjustment(const unsigned ledCnt, const QJsonObject & colorConfig) -{ - // Create the result, the transforms are added to this - MultiColorAdjustment * adjustment = new MultiColorAdjustment(ledCnt); - - const QJsonValue adjustmentConfig = colorConfig["channelAdjustment"]; - const QRegExp overallExp("([0-9]+(\\-[0-9]+)?)(,[ ]*([0-9]+(\\-[0-9]+)?))*"); - - const QJsonArray & adjustmentConfigArray = adjustmentConfig.toArray(); - for (signed i = 0; i < adjustmentConfigArray.size(); ++i) - { - const QJsonObject & config = adjustmentConfigArray.at(i).toObject(); - ColorAdjustment * colorAdjustment = createColorAdjustment(config); - adjustment->addAdjustment(colorAdjustment); - - const QString ledIndicesStr = config["leds"].toString("").trimmed(); - if (ledIndicesStr.compare("*") == 0) - { - // Special case for indices '*' => all leds - adjustment->setAdjustmentForLed(colorAdjustment->_id, 0, ledCnt-1); - Info(CORE_LOGGER, "ColorAdjustment '%s' => [0; %d]", QSTRING_CSTR(colorAdjustment->_id), ledCnt-1); - continue; - } - - if (!overallExp.exactMatch(ledIndicesStr)) - { - Error(CORE_LOGGER, "Given led indices %d not correct format: %s", i, QSTRING_CSTR(ledIndicesStr)); - continue; - } - - std::stringstream ss; - const QStringList ledIndexList = ledIndicesStr.split(","); - for (int i=0; i 0) - { - ss << ", "; - } - if (ledIndexList[i].contains("-")) - { - QStringList ledIndices = ledIndexList[i].split("-"); - int startInd = ledIndices[0].toInt(); - int endInd = ledIndices[1].toInt(); - - adjustment->setAdjustmentForLed(colorAdjustment->_id, startInd, endInd); - ss << startInd << "-" << endInd; - } - else - { - int index = ledIndexList[i].toInt(); - adjustment->setAdjustmentForLed(colorAdjustment->_id, index, index); - ss << index; - } - } - Info(CORE_LOGGER, "ColorAdjustment '%s' => [%s]", QSTRING_CSTR(colorAdjustment->_id), ss.str().c_str()); - } - - return adjustment; -} - -RgbTransform* Hyperion::createRgbTransform(const QJsonObject& colorConfig) -{ - const double backlightThreshold = colorConfig["backlightThreshold"].toDouble(0.0); - const bool backlightColored = colorConfig["backlightColored"].toBool(false); - const double brightness = colorConfig["brightness"].toInt(100); - const double brightnessComp= colorConfig["brightnessCompensation"].toInt(100); - const double gammaR = colorConfig["gammaRed"].toDouble(1.0); - const double gammaG = colorConfig["gammaGreen"].toDouble(1.0); - const double gammaB = colorConfig["gammaBlue"].toDouble(1.0); - - RgbTransform* transform = new RgbTransform(gammaR, gammaG, gammaB, backlightThreshold, backlightColored, brightness, brightnessComp); - return transform; -} - -RgbChannelAdjustment* Hyperion::createRgbChannelAdjustment(const QJsonObject& colorConfig, const QString channelName, const int defaultR, const int defaultG, const int defaultB) -{ - const QJsonArray& channelConfig = colorConfig[channelName].toArray(); - RgbChannelAdjustment* adjustment = new RgbChannelAdjustment( - channelConfig[0].toInt(defaultR), - channelConfig[1].toInt(defaultG), - channelConfig[2].toInt(defaultB), - "ChannelAdjust_"+channelName.toUpper()); - return adjustment; -} - -LedString Hyperion::createLedString(const QJsonValue& ledsConfig, const ColorOrder deviceOrder) -{ - LedString ledString; - const QString deviceOrderStr = colorOrderToString(deviceOrder); - const QJsonArray & ledConfigArray = ledsConfig.toArray(); - int maxLedId = ledConfigArray.size(); - - for (signed i = 0; i < ledConfigArray.size(); ++i) - { - const QJsonObject& index = ledConfigArray[i].toObject(); - - Led led; - led.index = index["index"].toInt(); - led.clone = index["clone"].toInt(-1); - if ( led.clone < -1 || led.clone >= maxLedId ) - { - Warning(CORE_LOGGER, "LED %d: clone index of %d is out of range, clone ignored", led.index, led.clone); - led.clone = -1; - } - - if ( led.clone < 0 ) - { - const QJsonObject& hscanConfig = ledConfigArray[i].toObject()["hscan"].toObject(); - const QJsonObject& vscanConfig = ledConfigArray[i].toObject()["vscan"].toObject(); - led.minX_frac = qMax(0.0, qMin(1.0, hscanConfig["minimum"].toDouble())); - led.maxX_frac = qMax(0.0, qMin(1.0, hscanConfig["maximum"].toDouble())); - led.minY_frac = qMax(0.0, qMin(1.0, vscanConfig["minimum"].toDouble())); - led.maxY_frac = qMax(0.0, qMin(1.0, vscanConfig["maximum"].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(index["colorOrder"].toString(deviceOrderStr)); - ledString.leds().push_back(led); - } - } - - // Make sure the leds are sorted (on their indices) - std::sort(ledString.leds().begin(), ledString.leds().end(), [](const Led& lhs, const Led& rhs){ return lhs.index < rhs.index; }); - return ledString; -} - -LedString Hyperion::createLedStringClone(const QJsonValue& ledsConfig, const ColorOrder deviceOrder) -{ - LedString ledString; - const QString deviceOrderStr = colorOrderToString(deviceOrder); - const QJsonArray & ledConfigArray = ledsConfig.toArray(); - int maxLedId = ledConfigArray.size(); - - for (signed i = 0; i < ledConfigArray.size(); ++i) - { - const QJsonObject& index = ledConfigArray[i].toObject(); - - Led led; - led.index = index["index"].toInt(); - led.clone = index["clone"].toInt(-1); - if ( led.clone < -1 || led.clone >= maxLedId ) - { - Warning(CORE_LOGGER, "LED %d: clone index of %d is out of range, clone ignored", led.index, led.clone); - led.clone = -1; - } - - if ( led.clone >= 0 ) - { - Debug(CORE_LOGGER, "LED %d: clone from led %d", led.index, led.clone); - led.minX_frac = 0; - led.maxX_frac = 0; - led.minY_frac = 0; - led.maxY_frac = 0; - // Get the order of the rgb channels for this led (default is device order) - led.colorOrder = stringToColorOrder(index["colorOrder"].toString(deviceOrderStr)); - - ledString.leds().push_back(led); - } - - } - - // Make sure the leds are sorted (on their indices) - std::sort(ledString.leds().begin(), ledString.leds().end(), [](const Led& lhs, const Led& rhs){ return lhs.index < rhs.index; }); - return ledString; -} - -QSize Hyperion::getLedLayoutGridSize(const QJsonValue& ledsConfig) -{ - std::vector midPointsX; - std::vector midPointsY; - const QJsonArray & ledConfigArray = ledsConfig.toArray(); - - for (signed i = 0; i < ledConfigArray.size(); ++i) - { - const QJsonObject& index = ledConfigArray[i].toObject(); - - if (index["clone"].toInt(-1) < 0 ) - { - const QJsonObject& hscanConfig = ledConfigArray[i].toObject()["hscan"].toObject(); - const QJsonObject& vscanConfig = ledConfigArray[i].toObject()["vscan"].toObject(); - double minX_frac = qMax(0.0, qMin(1.0, hscanConfig["minimum"].toDouble())); - double maxX_frac = qMax(0.0, qMin(1.0, hscanConfig["maximum"].toDouble())); - double minY_frac = qMax(0.0, qMin(1.0, vscanConfig["minimum"].toDouble())); - double maxY_frac = qMax(0.0, qMin(1.0, vscanConfig["maximum"].toDouble())); - // Fix if the user swapped min and max - if (minX_frac > maxX_frac) - { - std::swap(minX_frac, maxX_frac); - } - if (minY_frac > maxY_frac) - { - std::swap(minY_frac, maxY_frac); - } - - // calculate mid point and make grid calculation - midPointsX.push_back( int(1000.0*(minX_frac + maxX_frac) / 2.0) ); - midPointsY.push_back( int(1000.0*(minY_frac + maxY_frac) / 2.0) ); - } - } - - // remove duplicates - std::sort(midPointsX.begin(), midPointsX.end()); - midPointsX.erase(std::unique(midPointsX.begin(), midPointsX.end()), midPointsX.end()); - std::sort(midPointsY.begin(), midPointsY.end()); - midPointsY.erase(std::unique(midPointsY.begin(), midPointsY.end()), midPointsY.end()); - - QSize gridSize( midPointsX.size(), midPointsY.size() ); - Debug(CORE_LOGGER, "led layout grid: %dx%d", gridSize.width(), gridSize.height()); - - return gridSize; -} - - - -LinearColorSmoothing * Hyperion::createColorSmoothing(const QJsonObject & smoothingConfig, LedDevice* leddevice){ - QString type = smoothingConfig["type"].toString("linear").toLower(); - LinearColorSmoothing * device = nullptr; - type = "linear"; // TODO currently hardcoded type, delete it if we have more types - - if (type == "linear") - { - Info( CORE_LOGGER, "Creating linear smoothing"); - device = new LinearColorSmoothing( - leddevice, - smoothingConfig["updateFrequency"].toDouble(25.0), - smoothingConfig["time_ms"].toInt(200), - smoothingConfig["updateDelay"].toInt(0), - smoothingConfig["continuousOutput"].toBool(true) - ); - } - else - { - Error(CORE_LOGGER, "Smoothing disabled, because of unknown type '%s'.", QSTRING_CSTR(type)); - } - - device->setEnable(smoothingConfig["enable"].toBool(true)); - InfoIf(!device->enabled(), CORE_LOGGER,"Smoothing disabled"); - - Q_ASSERT(device != nullptr); - return device; -} - -MessageForwarder * Hyperion::createMessageForwarder(const QJsonObject & forwarderConfig) -{ - MessageForwarder * forwarder = new MessageForwarder(); - if ( !forwarderConfig.isEmpty() && forwarderConfig["enable"].toBool(true) ) - { - if ( !forwarderConfig["json"].isNull() && forwarderConfig["json"].isArray() ) - { - const QJsonArray & addr = forwarderConfig["json"].toArray(); - for (signed i = 0; i < addr.size(); ++i) - { - Info(CORE_LOGGER, "Json forward to %s", addr.at(i).toString().toStdString().c_str()); - forwarder->addJsonSlave(addr[i].toString()); - } - } - - if ( !forwarderConfig["proto"].isNull() && forwarderConfig["proto"].isArray() ) - { - const QJsonArray & addr = forwarderConfig["proto"].toArray(); - for (signed i = 0; i < addr.size(); ++i) - { - Info(CORE_LOGGER, "Proto forward to %s", addr.at(i).toString().toStdString().c_str()); - forwarder->addProtoSlave(addr[i].toString()); - } - } - } - - return forwarder; -} - -MessageForwarder * Hyperion::getForwarder() -{ - return _messageForwarder; -} - -Hyperion::Hyperion(const QJsonObject &qjsonConfig, const QString configFile, const QString rootPath) - : _ledString(createLedString(qjsonConfig["leds"], createColorOrder(qjsonConfig["device"].toObject()))) - , _ledStringClone(createLedStringClone(qjsonConfig["leds"], createColorOrder(qjsonConfig["device"].toObject()))) +Hyperion::Hyperion(HyperionDaemon* daemon, const quint8& instance, const QString configFile, const QString rootPath) + : _daemon(daemon) + , _settingsManager(new SettingsManager(this, instance, configFile)) + , _componentRegister(this) + , _ledString(hyperion::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) + , _ledStringClone(hyperion::createLedStringClone(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) + , _imageProcessor(new ImageProcessor(_ledString, this)) , _muxer(_ledString.leds().size()) - , _raw2ledAdjustment(createLedColorsAdjustment(_ledString.leds().size(), qjsonConfig["color"].toObject())) + , _raw2ledAdjustment(hyperion::createLedColorsAdjustment(_ledString.leds().size(), getSetting(settings::COLOR).object())) , _effectEngine(nullptr) - , _messageForwarder(createMessageForwarder(qjsonConfig["forwarder"].toObject())) - , _qjsonConfig(qjsonConfig) + , _messageForwarder(new MessageForwarder(this)) , _configFile(configFile) , _rootPath(rootPath) - , _timer() - , _timerBonjourResolver() - , _log(CORE_LOGGER) - , _hwLedCount(_ledString.leds().size()) - , _sourceAutoSelectEnabled(true) + , _log(Logger::getInstance("HYPERION")) + , _hwLedCount() , _configHash() - , _ledGridSize(getLedLayoutGridSize(qjsonConfig["leds"])) + , _ledGridSize(hyperion::getLedLayoutGridSize(getSetting(settings::LEDS).array())) , _prevCompId(hyperion::COMP_INVALID) - , _bonjourBrowser(this) - , _bonjourResolver(this) - , _videoMode(VIDEO_2D) + , _ledBuffer(_ledString.leds().size(), ColorRgb::BLACK) { - if (!_raw2ledAdjustment->verifyAdjustments()) + Warning(_log, "At least one led has no color calibration, please add all leds from your led layout to an 'LED index' field!"); + + // handle hwLedCount + _hwLedCount = qMax(unsigned(getSetting(settings::DEVICE).object()["hardwareLedCount"].toInt(getLedCount())), getLedCount()); + + // init colororder vector + for (Led& led : _ledString.leds()) { - throw std::runtime_error("Color adjustment incorrectly set"); + _ledStringColorOrder.push_back(led.colorOrder); } + + for (Led& led : _ledStringClone.leds()) + { + _ledStringColorOrder.insert(_ledStringColorOrder.begin() + led.index, led.colorOrder); + } + + // connect Hyperion::update with Muxer visible priority changes as muxer updates independent + connect(&_muxer, &PriorityMuxer::visiblePriorityChanged, this, &Hyperion::update); + + // listens for ComponentRegister changes of COMP_ALL to perform core enable/disable actions + connect(&_componentRegister, &ComponentRegister::updatedComponentState, this, &Hyperion::updatedComponentState); + + // listen for settings updates of this instance (LEDS & COLOR) + connect(_settingsManager, &SettingsManager::settingsChanged, this, &Hyperion::handleSettingsUpdate); + // set color correction activity state - const QJsonObject& color = qjsonConfig["color"].toObject(); - - _bonjourBrowser.browseForServiceType(QLatin1String("_hyperiond-http._tcp")); - connect(&_bonjourBrowser, SIGNAL(currentBonjourRecordsChanged(const QList&)),this, SLOT(currentBonjourRecordsChanged(const QList &))); - connect(&_bonjourResolver, SIGNAL(bonjourRecordResolved(const QHostInfo &, int)), this, SLOT(bonjourRecordResolved(const QHostInfo &, int))); - - // initialize the image processor factory - _ledMAppingType = ImageProcessor::mappingTypeToInt(color["imageToLedMappingType"].toString()); - ImageProcessorFactory::getInstance().init(_ledString, qjsonConfig["blackborderdetector"].toObject(),_ledMAppingType ); - - getComponentRegister().componentStateChanged(hyperion::COMP_FORWARDER, _messageForwarder->forwardingEnabled()); + const QJsonObject color = getSetting(settings::COLOR).object(); // initialize leddevices - _device = LedDeviceFactory::construct(qjsonConfig["device"].toObject(),_hwLedCount); - _deviceSmooth = createColorSmoothing(qjsonConfig["smoothing"].toObject(), _device); - getComponentRegister().componentStateChanged(hyperion::COMP_SMOOTHING, _deviceSmooth->componentState()); - getComponentRegister().componentStateChanged(hyperion::COMP_LEDDEVICE, _device->componentState()); + QJsonObject ledDevice = getSetting(settings::DEVICE).object(); + ledDevice["currentLedCount"] = int(_hwLedCount); // Inject led count info - _deviceSmooth->addConfig(true); // add pause to config 1 + _ledDeviceWrapper = new LedDeviceWrapper(this); + connect(this, &Hyperion::componentStateChanged, _ledDeviceWrapper, &LedDeviceWrapper::handleComponentState); + connect(this, &Hyperion::ledDeviceData, _ledDeviceWrapper, &LedDeviceWrapper::write); + _ledDeviceWrapper->createLedDevice(ledDevice); - // setup the timer - _timer.setSingleShot(true); - QObject::connect(&_timer, SIGNAL(timeout()), this, SLOT(update())); + // smoothing + _deviceSmooth = new LinearColorSmoothing(getSetting(settings::SMOOTHING), this); + connect(this, &Hyperion::settingsChanged, _deviceSmooth, &LinearColorSmoothing::handleSettingsUpdate); - _timerBonjourResolver.setSingleShot(false); - _timerBonjourResolver.setInterval(1000); - QObject::connect(&_timerBonjourResolver, SIGNAL(timeout()), this, SLOT(bonjourResolve())); - _timerBonjourResolver.start(); - - // create the effect engine, must be initialized after smoothing! - _effectEngine = new EffectEngine(this,qjsonConfig["effects"].toObject()); - - const QJsonObject& device = qjsonConfig["device"].toObject(); - unsigned int hwLedCount = device["ledCount"].toInt(getLedCount()); - _hwLedCount = qMax(hwLedCount, getLedCount()); - Debug(_log,"configured leds: %d hw leds: %d", getLedCount(), _hwLedCount); - WarningIf(hwLedCount < getLedCount(), _log, "more leds configured than available. check 'ledCount' in 'device' section"); + // create the effect engine; needs to be initialized after smoothing! + _effectEngine = new EffectEngine(this); + connect(_effectEngine, &EffectEngine::effectListUpdated, this, &Hyperion::effectListUpdated); // setup config state checks and initial shot checkConfigState(); if(_fsWatcher.addPath(_configFile)) - { QObject::connect(&_fsWatcher, &QFileSystemWatcher::fileChanged, this, &Hyperion::checkConfigState); - } else { + _cTimer = new QTimer(this); Warning(_log,"Filesystem Observer failed for file: %s, use fallback timer", _configFile.toStdString().c_str()); - QObject::connect(&_cTimer, SIGNAL(timeout()), this, SLOT(checkConfigState())); - _cTimer.start(2000); + connect(_cTimer, SIGNAL(timeout()), this, SLOT(checkConfigState())); + _cTimer->start(2000); } - // pipe muxer signal for effect/color timerunner to hyperionStateChanged slot - QObject::connect(&_muxer, &PriorityMuxer::timerunner, this, &Hyperion::hyperionStateChanged); + // initial startup effect + hyperion::handleInitialEffect(this, getSetting(settings::FGEFFECT).object()); - // prepare processing of hyperionStateChanged for forced serverinfo - connect(&_fsi_timer, SIGNAL(timeout()), this, SLOT(hyperionStateChanged())); - _fsi_timer.setSingleShot(true); - _fsi_blockTimer.setSingleShot(true); + // handle background effect + _BGEffectHandler = new BGEffectHandler(this); - // initialize the leds + // create the Daemon capture interface + _captureCont = new CaptureCont(this); + + // if there is no startup / background eff and no sending capture interface we probably want to push once BLACK (as PrioMuxer won't emit a prioritiy change) update(); -} -int Hyperion::getLatchTime() const -{ - return _device->getLatchTime(); -} - -unsigned Hyperion::addSmoothingConfig(int settlingTime_ms, double ledUpdateFrequency_hz, unsigned updateDelay) -{ - return _deviceSmooth->addConfig(settlingTime_ms, ledUpdateFrequency_hz, updateDelay); -} - -void Hyperion::freeObjects(bool emitCloseSignal) -{ - // switch off all leds - clearall(true); - _device->switchOff(); - - if (emitCloseSignal) - { - emit closing(); - } - - - // delete components on exit of hyperion core - delete _effectEngine; - delete _device; - delete _raw2ledAdjustment; - delete _messageForwarder; + // boblight, can't live in global scope as it depends on layout + _boblightServer = new BoblightServer(this, getSetting(settings::BOBLSERVER)); + connect(this, &Hyperion::settingsChanged, _boblightServer, &BoblightServer::handleSettingsUpdate); } Hyperion::~Hyperion() @@ -506,58 +168,149 @@ Hyperion::~Hyperion() freeObjects(false); } +void Hyperion::freeObjects(bool emitCloseSignal) +{ + // switch off all leds + clearall(true); + + if (emitCloseSignal) + { + emit closing(); + } + + // delete components on exit of hyperion core + delete _boblightServer; + delete _captureCont; + delete _effectEngine; + //delete _deviceSmooth; + delete _raw2ledAdjustment; + delete _messageForwarder; + delete _settingsManager; + delete _ledDeviceWrapper; +} + +void Hyperion::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::COLOR) + { + const QJsonObject obj = config.object(); + // change in color recreate ledAdjustments + delete _raw2ledAdjustment; + _raw2ledAdjustment = hyperion::createLedColorsAdjustment(_ledString.leds().size(), obj); + + if (!_raw2ledAdjustment->verifyAdjustments()) + { + Warning(_log, "At least one led has no color calibration, please add all leds from your led layout to an 'LED index' field!"); + } + } + else if(type == settings::LEDS) + { + const QJsonArray leds = config.array(); + + // lock update() + _lockUpdate = true; + // stop and cache all running effects, as effects depend heavily on ledlayout + _effectEngine->cacheRunningEffects(); + + // ledstring, clone, img processor, muxer, ledGridSize (eff engine image based effects), _ledBuffer and ByteOrder of ledstring + _ledString = hyperion::createLedString(leds, hyperion::createColorOrder(getSetting(settings::DEVICE).object())); + _ledStringClone = hyperion::createLedStringClone(leds, hyperion::createColorOrder(getSetting(settings::DEVICE).object())); + _imageProcessor->setLedString(_ledString); + _muxer.updateLedColorsLength(_ledString.leds().size()); + _ledGridSize = hyperion::getLedLayoutGridSize(leds); + + std::vector color(_ledString.leds().size(), ColorRgb{0,0,0}); + _ledBuffer = color; + + _ledStringColorOrder.clear(); + for (Led& led : _ledString.leds()) + { + _ledStringColorOrder.push_back(led.colorOrder); + } + for (Led& led : _ledStringClone.leds()) + { + _ledStringColorOrder.insert(_ledStringColorOrder.begin() + led.index, led.colorOrder); + } + + // handle hwLedCount update + _hwLedCount = qMax(unsigned(getSetting(settings::DEVICE).object()["hardwareLedCount"].toInt(getLedCount())), getLedCount()); + + // update led count in device + //_ledDeviceWrapper->setLedCount(_hwLedCount); + + // change in leds are also reflected in adjustment + delete _raw2ledAdjustment; + _raw2ledAdjustment = hyperion::createLedColorsAdjustment(_ledString.leds().size(), getSetting(settings::COLOR).object()); + + // start cached effects + _effectEngine->startCachedEffects(); + + // unlock + _lockUpdate = false; + } + else if(type == settings::DEVICE) + { + _lockUpdate = true; + QJsonObject dev = config.object(); + + // handle hwLedCount update + _hwLedCount = qMax(unsigned(dev["hardwareLedCount"].toInt(getLedCount())), getLedCount()); + + // force ledString update, if device ByteOrder changed + if(_ledDeviceWrapper->getColorOrder() != dev["colorOrder"].toString("rgb")) + { + _ledString = hyperion::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(dev)); + _ledStringClone = hyperion::createLedStringClone(getSetting(settings::LEDS).array(), hyperion::createColorOrder(dev)); + _imageProcessor->setLedString(_ledString); + } + + /* // reinit led device type on change + if(_device->getActiveDevice() != dev["type"].toString("file").toLower()) + { + } + // update led count + _device->setLedCount(_hwLedCount); + */ + // do always reinit until the led devices can handle dynamic changes + dev["currentLedCount"] = int(_hwLedCount); // Inject led count info + _ledDeviceWrapper->createLedDevice(dev); + _lockUpdate = false; + } + // update once to push single color sets / adjustments/ ledlayout resizes and update ledBuffer color + update(); +} + +QJsonDocument Hyperion::getSetting(const settings::type& type) +{ + return _settingsManager->getSetting(type); +} + +bool Hyperion::saveSettings(QJsonObject config, const bool& correct) +{ + return _settingsManager->saveSettings(config, correct); +} + +QString Hyperion::getConfigFileName() const +{ + QFileInfo cF(_configFile); + return cF.fileName(); +} + +int Hyperion::getLatchTime() const +{ + return _ledDeviceWrapper->getLatchTime(); +} + +unsigned Hyperion::addSmoothingConfig(int settlingTime_ms, double ledUpdateFrequency_hz, unsigned updateDelay) +{ + return _deviceSmooth->addConfig(settlingTime_ms, ledUpdateFrequency_hz, updateDelay); +} + unsigned Hyperion::getLedCount() const { return _ledString.leds().size(); } -void Hyperion::currentBonjourRecordsChanged(const QList &list) -{ - _hyperionSessions.clear(); - for ( auto rec : list ) - { - _hyperionSessions.insert(rec.serviceName, rec); - } -} - -void Hyperion::bonjourRecordResolved(const QHostInfo &hostInfo, int port) -{ - if ( _hyperionSessions.contains(_bonjourCurrentServiceToResolve)) - { - QString host = hostInfo.hostName(); - QString domain = _hyperionSessions[_bonjourCurrentServiceToResolve].replyDomain; - if (host.endsWith("."+domain)) - { - host.remove(host.length()-domain.length()-1,domain.length()+1); - } - _hyperionSessions[_bonjourCurrentServiceToResolve].hostName = host; - _hyperionSessions[_bonjourCurrentServiceToResolve].port = port; - _hyperionSessions[_bonjourCurrentServiceToResolve].address = hostInfo.addresses().isEmpty() ? "" : hostInfo.addresses().first().toString(); - Debug(_log, "found hyperion session: %s:%d",QSTRING_CSTR(hostInfo.hostName()), port); - - //emit change - emit hyperionStateChanged(); - } -} - -void Hyperion::bonjourResolve() -{ - for(auto key : _hyperionSessions.keys()) - { - if (_hyperionSessions[key].port < 0) - { - _bonjourCurrentServiceToResolve = key; - _bonjourResolver.resolveBonjourRecord(_hyperionSessions[key]); - break; - } - } -} - -Hyperion::BonjourRegister Hyperion::getHyperionSessions() -{ - return _hyperionSessions; -} - void Hyperion::checkConfigState(QString cfile) { // Check config modifications @@ -578,7 +331,6 @@ void Hyperion::checkConfigState(QString cfile) if(_prevConfigMod != _configMod) { - emit hyperionStateChanged(); _prevConfigMod = _configMod; } @@ -589,116 +341,94 @@ void Hyperion::checkConfigState(QString cfile) if(_prevConfigWrite != _configWrite) { - emit hyperionStateChanged(); _prevConfigWrite = _configWrite; } } -void Hyperion::registerPriority(const QString &name, const int priority/*, const QString &origin*/) -{ - Info(_log, "Register new input source named '%s' for priority channel '%d'", QSTRING_CSTR(name), priority ); - - for(auto key : _priorityRegister.keys()) - { - WarningIf( ( key != name && _priorityRegister.value(key) == priority), _log, - "Input source '%s' uses same priority channel (%d) as '%s'.", QSTRING_CSTR(name), priority, QSTRING_CSTR(key)); - } - - _priorityRegister.insert(name, priority); - emit hyperionStateChanged(); -} - -void Hyperion::unRegisterPriority(const QString &name) -{ - Info(_log, "Unregister input source named '%s' from priority register", QSTRING_CSTR(name)); - _priorityRegister.remove(name); - emit hyperionStateChanged(); -} - void Hyperion::setSourceAutoSelectEnabled(bool enabled) { - _sourceAutoSelectEnabled = enabled; - if (! _sourceAutoSelectEnabled) - { - setCurrentSourcePriority(_muxer.getCurrentPriority()); - } - update(); - DebugIf( !_sourceAutoSelectEnabled, _log, "source auto select is disabled"); - InfoIf(_sourceAutoSelectEnabled, _log, "set current input source to auto select"); + if(_muxer.setSourceAutoSelectEnabled(enabled)) + update(); } bool Hyperion::setCurrentSourcePriority(int priority ) { - bool priorityValid = _muxer.hasPriority(priority); - if (priorityValid) - { - DebugIf(_sourceAutoSelectEnabled, _log, "source auto select is disabled"); - _sourceAutoSelectEnabled = false; - _currentSourcePriority = priority; - Info(_log, "set current input source to priority channel %d", _currentSourcePriority); - } + return _muxer.setPriority(priority); +} - return priorityValid; +bool Hyperion::sourceAutoSelectEnabled() +{ + return _muxer.isSourceAutoSelectEnabled(); +} + +void Hyperion::setNewComponentState(const hyperion::Components& component, const bool& state) +{ + _componentRegister.componentStateChanged(component, state); } void Hyperion::setComponentState(const hyperion::Components component, const bool state) { - switch (component) - { - case hyperion::COMP_SMOOTHING: - _deviceSmooth->setEnable(state); - getComponentRegister().componentStateChanged(hyperion::COMP_SMOOTHING, _deviceSmooth->componentState()); - break; - - case hyperion::COMP_LEDDEVICE: - _device->setEnable(state); - getComponentRegister().componentStateChanged(hyperion::COMP_LEDDEVICE, _device->componentState()); - break; - - default: - emit componentStateChanged(component, state); - } + emit componentStateChanged(component, state); } -void Hyperion::setColor(int priority, const ColorRgb &color, const int timeout_ms, bool clearEffects) +void Hyperion::registerInput(const int priority, const hyperion::Components& component, const QString& origin, const QString& owner, unsigned smooth_cfg) { - // create led output + _muxer.registerInput(priority, component, origin, owner, smooth_cfg); +} + +const bool Hyperion::setInput(const int priority, const std::vector& ledColors, int timeout_ms, const bool& clearEffect) +{ + if(_muxer.setInput(priority, ledColors, timeout_ms)) + { + // clear effect if this call does not come from an effect + if(clearEffect) + _effectEngine->channelCleared(priority); + + // if this priority is visible, update immediately + if(priority == _muxer.getCurrentPriority()) + update(); + + return true; + } + return false; +} + +const bool Hyperion::setInputImage(const int priority, const Image& image, int64_t timeout_ms, const bool& clearEffect) +{ + if(_muxer.setInputImage(priority, image, timeout_ms)) + { + // clear effect if this call does not come from an effect + if(clearEffect) + _effectEngine->channelCleared(priority); + + // if this priority is visible, update immediately + if(priority == _muxer.getCurrentPriority()) + update(); + + return true; + } + return false; +} + +const bool Hyperion::setInputInactive(const quint8& priority) +{ + return _muxer.setInputInactive(priority); +} + +void Hyperion::setColor(int priority, const ColorRgb &color, const int timeout_ms, const QString& origin, bool clearEffects) +{ + // clear effect if this call does not come from an effect + if(clearEffects) + _effectEngine->channelCleared(priority); + + // create led vector from single color std::vector ledColors(_ledString.leds().size(), color); - // set colors - setColors(priority, ledColors, timeout_ms, clearEffects, hyperion::COMP_COLOR); -} + // register color + registerInput(priority, hyperion::COMP_COLOR, origin); -void Hyperion::setColors(int priority, const std::vector& ledColors, const int timeout_ms, bool clearEffects, hyperion::Components component, const QString origin, unsigned smoothCfg) -{ - // clear effects if this call does not come from an effect - if (clearEffects) - { - _effectEngine->channelCleared(priority); - } - - if (timeout_ms > 0) - { - const uint64_t timeoutTime = QDateTime::currentMSecsSinceEpoch() + timeout_ms; - _muxer.setInput(priority, ledColors, timeoutTime, component, origin, smoothCfg); - } - else - { - _muxer.setInput(priority, ledColors, -1, component, origin, smoothCfg); - } - - if (! _sourceAutoSelectEnabled || priority == _muxer.getCurrentPriority()) - { - update(); - } -} - -void Hyperion::setImage(int priority, const Image & image, int duration_ms) -{ - if (priority == getCurrentPriority()) - { - emit emitImage(priority, image, duration_ms); - } + // write color to muxer + setInput(priority, ledColors, timeout_ms); } const QStringList & Hyperion::getAdjustmentIds() const @@ -713,38 +443,25 @@ ColorAdjustment * Hyperion::getAdjustment(const QString& id) void Hyperion::adjustmentsUpdated() { + emit adjustmentChanged(); update(); } -void Hyperion::clear(int priority) +const bool Hyperion::clear(int priority) { - if (_muxer.hasPriority(priority)) - { - _muxer.clearInput(priority); - if (!_sourceAutoSelectEnabled && _currentSourcePriority == priority ) - { - setSourceAutoSelectEnabled(true); - } - - // update leds if necessary - if (priority < _muxer.getCurrentPriority()) - { - update(); - } - } - // send clear signal to the effect engine // (outside the check so the effect gets cleared even when the effect is not sending colors) _effectEngine->channelCleared(priority); + + if(_muxer.clearInput(priority)) + return true; + + return false; } void Hyperion::clearall(bool forceClearAll) { _muxer.clearAll(forceClearAll); - setSourceAutoSelectEnabled(true); - - // update leds - update(); // send clearall signal to the effect engine _effectEngine->allChannelsCleared(); @@ -752,8 +469,7 @@ void Hyperion::clearall(bool forceClearAll) int Hyperion::getCurrentPriority() const { - - return _sourceAutoSelectEnabled || !_muxer.hasPriority(_currentSourcePriority) ? _muxer.getCurrentPriority() : _currentSourcePriority; + return _muxer.getCurrentPriority(); } bool Hyperion::isCurrentPriority(const int priority) const @@ -766,14 +482,19 @@ QList Hyperion::getActivePriorities() const return _muxer.getPriorities(); } -const Hyperion::InputInfo &Hyperion::getPriorityInfo(const int priority) const +const Hyperion::InputInfo Hyperion::getPriorityInfo(const int priority) const { return _muxer.getInputInfo(priority); } -void Hyperion::reloadEffects() +const bool Hyperion::saveEffect(const QJsonObject& obj, QString& resultMsg) { - _effectEngine->readEffects(); + return _effectEngine->saveEffect(obj, resultMsg); +} + +const bool Hyperion::deleteEffect(const QString& effectName, QString& resultMsg) +{ + return _effectEngine->deleteEffect(effectName, resultMsg); } const std::list & Hyperion::getEffects() const @@ -791,75 +512,113 @@ const std::list & Hyperion::getEffectSchemas() return _effectEngine->getEffectSchemas(); } +const QJsonObject& Hyperion::getQJsonConfig() +{ + return _settingsManager->getSettings(); +} + int Hyperion::setEffect(const QString &effectName, int priority, int timeout, const QString & origin) { return _effectEngine->runEffect(effectName, priority, timeout, origin); } -int Hyperion::setEffect(const QString &effectName, const QJsonObject &args, int priority, int timeout, const QString & pythonScript, const QString & origin) +int Hyperion::setEffect(const QString &effectName, const QJsonObject &args, int priority, int timeout, const QString &pythonScript, const QString &origin, const QString &imageData) { - return _effectEngine->runEffect(effectName, args, priority, timeout, pythonScript, origin); + return _effectEngine->runEffect(effectName, args, priority, timeout, pythonScript, origin, 0, imageData); } -void Hyperion::setLedMappingType(int mappingType) +void Hyperion::setLedMappingType(const int& mappingType) { - _ledMAppingType = mappingType; - emit imageToLedsMappingChanged(mappingType); + if(mappingType != _imageProcessor->getUserLedMappingType()) + { + _imageProcessor->setLedMappingType(mappingType); + emit imageToLedsMappingChanged(mappingType); + } } -void Hyperion::setVideoMode(VideoMode mode) +const int & Hyperion::getLedMappingType() +{ + return _imageProcessor->getUserLedMappingType(); +} + +void Hyperion::setVideoMode(const VideoMode& mode) { - _videoMode = mode; emit videoMode(mode); } - -void Hyperion::hyperionStateChanged() +const VideoMode & Hyperion::getCurrentVideoMode() { - if(_fsi_blockTimer.isActive()) + return _daemon->getVideoMode(); +} + +const QString & Hyperion::getActiveDevice() +{ + return _ledDeviceWrapper->getActiveDevice(); +} + +void Hyperion::updatedComponentState(const hyperion::Components comp, const bool state) +{ + if(comp == hyperion::COMP_ALL) { - _fsi_timer.start(300); - } - else - { - emit sendServerInfo(); - _fsi_blockTimer.start(250); + if(state) + { + // first muxer to update all inputs + _muxer.setEnable(state); + } + else + { + _muxer.setEnable(state); + } } } void Hyperion::update() { - // Update the muxer, cleaning obsolete priorities - _muxer.setCurrentTime(QDateTime::currentMSecsSinceEpoch()); + if(_lockUpdate) + return; + + // the ledbuffer resize for hwledcount needs to be reverted + if(_hwLedCount > _ledBuffer.size()) + _ledBuffer.resize(getLedCount()); // Obtain the current priority channel - int priority = _sourceAutoSelectEnabled || !_muxer.hasPriority(_currentSourcePriority) ? _muxer.getCurrentPriority() : _currentSourcePriority; - const PriorityMuxer::InputInfo & priorityInfo = _muxer.getInputInfo(priority); - - // copy ledcolors to local buffer - _ledBuffer.reserve(_hwLedCount); - _ledBuffer = priorityInfo.ledColors; + int priority = _muxer.getCurrentPriority(); + const PriorityMuxer::InputInfo priorityInfo = _muxer.getInputInfo(priority); + // eval comp change + bool compChanged = false; if (priorityInfo.componentId != _prevCompId) { - bool backlightEnabled = (priorityInfo.componentId != hyperion::COMP_COLOR && priorityInfo.componentId != hyperion::COMP_EFFECT); - _raw2ledAdjustment->setBacklightEnabled(backlightEnabled); + compChanged = true; _prevCompId = priorityInfo.componentId; } - _raw2ledAdjustment->applyAdjustment(_ledBuffer); - // init colororder vector, if empty - if (_ledStringColorOrder.empty()) + // copy image & process OR copy ledColors from muxer + Image image = priorityInfo.image; + if(image.size() > 3) { - for (Led& led : _ledString.leds()) + emit currentImage(image); + // disable the black border detector for effects and ledmapping to 0 + if(compChanged) { - _ledStringColorOrder.push_back(led.colorOrder); - } - for (Led& led : _ledStringClone.leds()) - { - _ledStringColorOrder.insert(_ledStringColorOrder.begin() + led.index, led.colorOrder); + _imageProcessor->setBlackbarDetectDisable((_prevCompId == hyperion::COMP_EFFECT)); + _imageProcessor->setHardLedMappingType((_prevCompId == hyperion::COMP_EFFECT) ? 0 : -1); } + _imageProcessor->process(image, _ledBuffer); } + else + { + _ledBuffer = priorityInfo.ledColors; + } + + // emit rawLedColors before transform + emit rawLedColors(_ledBuffer); + + // apply adjustments + if(compChanged) + _raw2ledAdjustment->setBacklightEnabled((_prevCompId != hyperion::COMP_COLOR && _prevCompId != hyperion::COMP_EFFECT)); + + _raw2ledAdjustment->applyAdjustment(_ledBuffer); // insert cloned leds into buffer for (Led& led : _ledStringClone.leds()) @@ -870,8 +629,6 @@ void Hyperion::update() int i = 0; for (ColorRgb& color : _ledBuffer) { - //const ColorOrder ledColorOrder = leds.at(i).colorOrder; - // correct the color byte order switch (_ledStringColorOrder.at(i)) { @@ -899,14 +656,14 @@ void Hyperion::update() } i++; } - + // fill aditional hw leds with black if ( _hwLedCount > _ledBuffer.size() ) { _ledBuffer.resize(_hwLedCount, ColorRgb::BLACK); } // Write the data to the device - if (_device->enabled()) + if (_ledDeviceWrapper->enabled()) { _deviceSmooth->selectConfig(priorityInfo.smooth_cfg); @@ -915,19 +672,6 @@ void Hyperion::update() _deviceSmooth->setLedValues(_ledBuffer); if (! _deviceSmooth->enabled()) - _device->setLedValues(_ledBuffer); + emit ledDeviceData(_ledBuffer); } - - // Start the timeout-timer - if (priorityInfo.timeoutTime_ms <= 0) - { - _timer.stop(); - } - else - { - int timeout_ms = qMax(0, int(priorityInfo.timeoutTime_ms - QDateTime::currentMSecsSinceEpoch())); - // qMin() 200ms forced refresh if color is active to update priorityMuxer properly for forced serverinfo push - _timer.start(qMin(timeout_ms, 200)); - } - } diff --git a/libsrc/hyperion/ImageProcessor.cpp b/libsrc/hyperion/ImageProcessor.cpp index 1bebddc5..4c6269be 100644 --- a/libsrc/hyperion/ImageProcessor.cpp +++ b/libsrc/hyperion/ImageProcessor.cpp @@ -9,27 +9,56 @@ using namespace hyperion; -ImageProcessor::ImageProcessor(const LedString& ledString, const QJsonObject & blackborderConfig) - : QObject() +// global transform method +int ImageProcessor::mappingTypeToInt(QString mappingType) +{ + if (mappingType == "unicolor_mean" ) + return 1; + + return 0; +} +// global transform method +QString ImageProcessor::mappingTypeToStr(int mappingType) +{ + if (mappingType == 1 ) + return "unicolor_mean"; + + return "multicolor_mean"; +} + +ImageProcessor::ImageProcessor(const LedString& ledString, Hyperion* hyperion) + : QObject(hyperion) , _log(Logger::getInstance("BLACKBORDER")) , _ledString(ledString) - , _borderProcessor(new BlackBorderProcessor(blackborderConfig) ) + , _borderProcessor(new BlackBorderProcessor(hyperion, this)) , _imageToLeds(nullptr) , _mappingType(0) + , _userMappingType(0) + , _hardMappingType(0) + , _hyperion(hyperion) { -// this is when we want to change the mapping for all input sources -// connect(Hyperion::getInstance(), SIGNAL(imageToLedsMappingChanged(int)), this, SLOT(setLedMappingType(int))); + // init + handleSettingsUpdate(settings::COLOR, _hyperion->getSetting(settings::COLOR)); + // listen for changes in color - ledmapping + connect(_hyperion, &Hyperion::settingsChanged, this, &ImageProcessor::handleSettingsUpdate); } ImageProcessor::~ImageProcessor() { delete _imageToLeds; - delete _borderProcessor; } -unsigned ImageProcessor::getLedCount() const +void ImageProcessor::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) { - return _ledString.leds().size(); + if(type == settings::COLOR) + { + const QJsonObject& obj = config.object(); + int newType = mappingTypeToInt(obj["imageToLedMappingType"].toString()); + if(_userMappingType != newType) + { + setLedMappingType(newType); + } + } } void ImageProcessor::setSize(const unsigned width, const unsigned height) @@ -41,15 +70,30 @@ void ImageProcessor::setSize(const unsigned width, const unsigned height) } // Clean up the old buffer and mapping - delete _imageToLeds; + _imageToLeds = 0; // Construct a new buffer and mapping _imageToLeds = (width>0 && height>0) ? (new ImageToLedsMap(width, height, 0, 0, _ledString.leds())) : nullptr; } -void ImageProcessor::enableBlackBorderDetector(bool enable) +void ImageProcessor::setLedString(const LedString& ledString) { - _borderProcessor->setEnabled(enable); + _ledString = ledString; + + // get current width/height + const unsigned width = _imageToLeds->width(); + const unsigned height = _imageToLeds->height(); + + // Clean up the old buffer and mapping + _imageToLeds = 0; + + // Construct a new buffer and mapping + _imageToLeds = new ImageToLedsMap(width, height, 0, 0, _ledString.leds()); +} + +void ImageProcessor::setBlackbarDetectDisable(bool enable) +{ + _borderProcessor->setHardDisable(enable); } bool ImageProcessor::blackBorderDetectorEnabled() @@ -59,29 +103,23 @@ bool ImageProcessor::blackBorderDetectorEnabled() void ImageProcessor::setLedMappingType(int mapType) { - Debug(_log, "set led mapping to type %d", mapType); - _mappingType = mapType; + // if the _hardMappingType is >-1 we aren't allowed to overwrite it + _userMappingType = mapType; + Debug(_log, "set user led mapping to %s", QSTRING_CSTR(mappingTypeToStr(mapType))); + if(_hardMappingType == -1) + { + _mappingType = mapType; + } } -int ImageProcessor::ledMappingType() +void ImageProcessor::setHardLedMappingType(int mapType) { - return _mappingType; -} - -int ImageProcessor::mappingTypeToInt(QString mappingType) -{ - if (mappingType == "unicolor_mean" ) - return 1; - - return 0; -} - -QString ImageProcessor::mappingTypeToStr(int mappingType) -{ - if (mappingType == 1 ) - return "unicolor_mean"; - - return "multicolor_mean"; + // force the maptype, if set to -1 we use the last requested _userMappingType + _hardMappingType = mapType; + if(mapType == -1) + _mappingType = _userMappingType; + else + _mappingType = mapType; } bool ImageProcessor::getScanParameters(size_t led, double &hscanBegin, double &hscanEnd, double &vscanBegin, double &vscanEnd) const @@ -97,4 +135,3 @@ bool ImageProcessor::getScanParameters(size_t led, double &hscanBegin, double &h return false; } - diff --git a/libsrc/hyperion/ImageProcessorFactory.cpp b/libsrc/hyperion/ImageProcessorFactory.cpp deleted file mode 100644 index 7b7cfbbe..00000000 --- a/libsrc/hyperion/ImageProcessorFactory.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// Hyperion includes -#include -#include - -ImageProcessorFactory& ImageProcessorFactory::getInstance() -{ - static ImageProcessorFactory instance; - // Return the singleton instance - return instance; -} - -void ImageProcessorFactory::init(const LedString& ledString, const QJsonObject & blackborderConfig, int mappingType) -{ - _ledString = ledString; - _blackborderConfig = blackborderConfig; - _mappingType = mappingType; -} - -ImageProcessor* ImageProcessorFactory::newImageProcessor() const -{ - ImageProcessor* ip = new ImageProcessor(_ledString, _blackborderConfig); - ip->setLedMappingType(_mappingType); - - return ip; -} diff --git a/libsrc/hyperion/ImageToLedsMap.cpp b/libsrc/hyperion/ImageToLedsMap.cpp index d02e9c81..7f3c7d5e 100644 --- a/libsrc/hyperion/ImageToLedsMap.cpp +++ b/libsrc/hyperion/ImageToLedsMap.cpp @@ -47,19 +47,24 @@ ImageToLedsMap::ImageToLedsMap( minX_idx = qMin(minX_idx, xOffset + actualWidth - 1); if (minX_idx == maxX_idx) { - maxX_idx = minX_idx + 1; + maxX_idx++; } minY_idx = qMin(minY_idx, yOffset + actualHeight - 1); if (minY_idx == maxY_idx) { - maxY_idx = minY_idx + 1; + maxY_idx++; } // Add all the indices in the above defined rectangle to the indices for this led + const auto maxYLedCount = qMin(maxY_idx, yOffset+actualHeight); + const auto maxXLedCount = qMin(maxX_idx, xOffset+actualWidth); + std::vector ledColors; - for (unsigned y = minY_idx; y +#include #include "LinearColorSmoothing.h" #include @@ -8,38 +9,56 @@ using namespace hyperion; -// ledUpdateFrequency_hz = 0 > cause divide by zero! -LinearColorSmoothing::LinearColorSmoothing( LedDevice * ledDevice, double ledUpdateFrequency_hz, int settlingTime_ms, unsigned updateDelay, bool continuousOutput) - : LedDevice() - , _ledDevice(ledDevice) - , _updateInterval(1000 / ledUpdateFrequency_hz) - , _settlingTime(settlingTime_ms) - , _timer() - , _outputDelay(updateDelay) +LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument& config, Hyperion* hyperion) + : LedDevice(QJsonObject(), hyperion) + , _log(Logger::getInstance("SMOOTHING")) + , _hyperion(hyperion) + , _updateInterval(1000) + , _settlingTime(200) + , _timer(new QTimer(this)) + , _outputDelay(0) , _writeToLedsEnable(true) - , _continuousOutput(continuousOutput) + , _continuousOutput(false) , _pause(false) , _currentConfigId(0) { - _log = Logger::getInstance("Smoothing"); - _timer.setSingleShot(false); - _timer.setInterval(_updateInterval); + // set initial state to true, as LedDevice::enabled() is true by default + _hyperion->getComponentRegister().componentStateChanged(hyperion::COMP_SMOOTHING, true); + + // init cfg 0 (default) + _cfgList.append({false, 200, 25, 0}); + handleSettingsUpdate(settings::SMOOTHING, config); - selectConfig( addConfig(_settlingTime, ledUpdateFrequency_hz, updateDelay) ); - // add pause on cfg 1 - SMOOTHING_CFG cfg = {true, 100, 50, 0}; + SMOOTHING_CFG cfg = {true}; _cfgList.append(cfg); - Info( _log, "smoothing cfg %d: pause", _cfgList.count()-1); - connect(&_timer, SIGNAL(timeout()), this, SLOT(updateLeds())); + // listen for comp changes + connect(_hyperion, &Hyperion::componentStateChanged, this, &LinearColorSmoothing::componentStateChange); + // timer + connect(_timer, SIGNAL(timeout()), this, SLOT(updateLeds())); } LinearColorSmoothing::~LinearColorSmoothing() { - // Make sure to switch off the underlying led-device (because switchOff is no longer forwarded) - _ledDevice->switchOff(); - delete _ledDevice; + +} + +void LinearColorSmoothing::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::SMOOTHING) + { + QJsonObject obj = config.object(); + _continuousOutput = obj["continuousOutput"].toBool(true); + SMOOTHING_CFG cfg = {false, obj["time_ms"].toInt(200), unsigned(1000.0/obj["updateFrequency"].toDouble(25.0)), unsigned(obj["updateDelay"].toInt(0))}; + _cfgList[0] = cfg; + // if current id is 0, we need to apply the settings (forced) + if(!_currentConfigId) + selectConfig(0, true); + + if(enabled() != obj["enable"].toBool(true)) + setEnable(obj["enable"].toBool(true)); + } } int LinearColorSmoothing::write(const std::vector &ledValues) @@ -53,7 +72,7 @@ int LinearColorSmoothing::write(const std::vector &ledValues) _previousTime = QDateTime::currentMSecsSinceEpoch(); _previousValues = ledValues; - _timer.start(); + _timer->start(); } else { @@ -127,7 +146,8 @@ void LinearColorSmoothing::queueColors(const std::vector & ledColors) { // No output delay => immediate write if ( _writeToLedsEnable && !_pause) - _ledDevice->setLedValues(ledColors); + emit _hyperion->ledDeviceData(ledColors); + } else { @@ -142,7 +162,7 @@ void LinearColorSmoothing::queueColors(const std::vector & ledColors) { if (!_pause) { - _ledDevice->setLedValues(_outputQueue.front()); + emit _hyperion->ledDeviceData(_outputQueue.front()); } _outputQueue.pop_front(); } @@ -150,6 +170,11 @@ void LinearColorSmoothing::queueColors(const std::vector & ledColors) } } +void LinearColorSmoothing::componentStateChange(const hyperion::Components component, const bool state) +{ + if(component == hyperion::COMP_SMOOTHING) + setEnable(state); +} void LinearColorSmoothing::setEnable(bool enable) { @@ -157,9 +182,11 @@ void LinearColorSmoothing::setEnable(bool enable) if (!enable) { - _timer.stop(); + _timer->stop(); _previousValues.clear(); } + // update comp register + _hyperion->getComponentRegister().componentStateChanged(hyperion::COMP_SMOOTHING, enable); } void LinearColorSmoothing::setPause(bool pause) @@ -171,14 +198,14 @@ unsigned LinearColorSmoothing::addConfig(int settlingTime_ms, double ledUpdateFr { SMOOTHING_CFG cfg = {false, settlingTime_ms, int64_t(1000.0/ledUpdateFrequency_hz), updateDelay}; _cfgList.append(cfg); - - Info( _log, "smoothing cfg %d: interval: %d ms, settlingTime: %d ms, updateDelay: %d frames", _cfgList.count()-1, cfg.updateInterval, cfg.settlingTime, cfg.outputDelay ); + + //Debug( _log, "smoothing cfg %d: interval: %d ms, settlingTime: %d ms, updateDelay: %d frames", _cfgList.count()-1, cfg.updateInterval, cfg.settlingTime, cfg.outputDelay ); return _cfgList.count() - 1; } -bool LinearColorSmoothing::selectConfig(unsigned cfg) +bool LinearColorSmoothing::selectConfig(unsigned cfg, const bool& force) { - if (_currentConfigId == cfg) + if (_currentConfigId == cfg && !force) { return true; } @@ -191,20 +218,19 @@ bool LinearColorSmoothing::selectConfig(unsigned cfg) if (_cfgList[cfg].updateInterval != _updateInterval) { - _timer.stop(); + _timer->stop(); _updateInterval = _cfgList[cfg].updateInterval; - _timer.setInterval(_updateInterval); - _timer.start(); + _timer->setInterval(_updateInterval); + _timer->start(); } _currentConfigId = cfg; - InfoIf( enabled() && !_pause, _log, "set smoothing cfg: %d, interval: %d ms, settlingTime: %d ms, updateDelay: %d frames", _currentConfigId, _updateInterval, _settlingTime, _outputDelay ); - InfoIf( _pause, _log, "set smoothing cfg: %d, pause", _currentConfigId ); + //DebugIf( enabled() && !_pause, _log, "set smoothing cfg: %d, interval: %d ms, settlingTime: %d ms, updateDelay: %d frames", _currentConfigId, _updateInterval, _settlingTime, _outputDelay ); + DebugIf( _pause, _log, "set smoothing cfg: %d, pause", _currentConfigId ); return true; } - + // reset to default _currentConfigId = 0; return false; } - diff --git a/libsrc/hyperion/LinearColorSmoothing.h b/libsrc/hyperion/LinearColorSmoothing.h index 34aff620..e84b0384 100644 --- a/libsrc/hyperion/LinearColorSmoothing.h +++ b/libsrc/hyperion/LinearColorSmoothing.h @@ -5,13 +5,19 @@ // Qt includes -#include #include // hyperion incluse #include #include +// settings +#include + +class QTimer; +class Logger; +class Hyperion; + /// Linear Smooting class /// /// This class processes the requested led values and forwards them to the device after applying @@ -22,11 +28,10 @@ class LinearColorSmoothing : public LedDevice public: /// Constructor - /// @param LedDevice the led device - /// @param LedUpdatFrequency The frequency at which the leds will be updated (Hz) - /// @param settingTime The time after which the updated led values have been fully applied (sec) - /// @param updateDelay The number of frames to delay outgoing led updates - LinearColorSmoothing(LedDevice *ledDevice, double ledUpdateFrequency, int settlingTime, unsigned updateDelay, bool continuousOutput); + /// @param config The configuration document smoothing + /// @param hyperion The hyperion parent instance + /// + LinearColorSmoothing(const QJsonDocument& config, Hyperion* hyperion); /// Destructor virtual ~LinearColorSmoothing(); @@ -46,13 +51,44 @@ public: bool pause() { return _pause; } ; bool enabled() { return LedDevice::enabled() && !_pause; }; + /// + /// @brief Add a new smoothing cfg which can be used with selectConfig() + /// @param settlingTime_ms The buffer time + /// @param ledUpdateFrequency_hz The frequency of update + /// @param updateDelay The delay + /// + /// @return The index of the cfg which can be passed to selectConfig() + /// unsigned addConfig(int settlingTime_ms, double ledUpdateFrequency_hz=25.0, unsigned updateDelay=0); - bool selectConfig(unsigned cfg); + + /// + /// @brief select a smoothing cfg given by cfg index from addConfig() + /// @param cfg The index to use + /// @param force Overwrite in any case the current values (used for cfg 0 settings udpate) + /// + /// @return On success return else false (and falls back to cfg 0) + /// + bool selectConfig(unsigned cfg, const bool& force = false); + +public slots: + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit or this constructor + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); private slots: /// Timer callback which writes updated led values to the led device void updateLeds(); + /// + /// @brief Handle component state changes + /// @param component The component + /// @param state The requested state + /// + void componentStateChange(const hyperion::Components component, const bool state); + private: /** * Pushes the colors into the output queue and popping the head to the led-device @@ -61,8 +97,11 @@ private: */ void queueColors(const std::vector & ledColors); - /// The led device - LedDevice * _ledDevice; + /// Logger instance + Logger* _log; + + /// Hyperion instance + Hyperion* _hyperion; /// The interval at which to update the leds (msec) int64_t _updateInterval; @@ -71,7 +110,7 @@ private: int64_t _settlingTime; /// The Qt timer object - QTimer _timer; + QTimer * _timer; /// The timestamp at which the target data should be fully applied int64_t _targetTime; @@ -92,7 +131,7 @@ private: /// Prevent sending data to device when no intput data is sent bool _writeToLedsEnable; - + /// Flag for dis/enable continuous output to led device regardless there is new data or not bool _continuousOutput; @@ -107,8 +146,8 @@ private: unsigned outputDelay; }; - /// config list + /// smooth config list QVector _cfgList; - + unsigned _currentConfigId; }; diff --git a/libsrc/hyperion/MessageForwarder.cpp b/libsrc/hyperion/MessageForwarder.cpp index 9ddb9863..a8fc4594 100644 --- a/libsrc/hyperion/MessageForwarder.cpp +++ b/libsrc/hyperion/MessageForwarder.cpp @@ -1,56 +1,264 @@ // STL includes #include +// project includes #include +// hyperion includes +#include -MessageForwarder::MessageForwarder() +// utils includes +#include + +// qt includes +#include +#include + +#include + +MessageForwarder::MessageForwarder(Hyperion *hyperion) + : QObject() + , _hyperion(hyperion) + , _log(Logger::getInstance("NETFORWARDER")) + , _muxer(_hyperion->getMuxerInstance()) + , _forwarder_enabled(true) + , _priority(140) { + // get settings updates + connect(_hyperion, &Hyperion::settingsChanged, this, &MessageForwarder::handleSettingsUpdate); + + // component changes + connect(_hyperion, &Hyperion::componentStateChanged, this, &MessageForwarder::componentStateChanged); + + // connect with Muxer visible priority changes + connect(_muxer, &PriorityMuxer::visiblePriorityChanged, this, &MessageForwarder::handlePriorityChanges); + + // init + handleSettingsUpdate(settings::NETFORWARD, _hyperion->getSetting(settings::NETFORWARD)); } MessageForwarder::~MessageForwarder() { + while (!_forwardClients.isEmpty()) + delete _forwardClients.takeFirst(); } +void MessageForwarder::handleSettingsUpdate(const settings::type &type, const QJsonDocument &config) +{ + if(type == settings::NETFORWARD) + { + // clear the current targets + _jsonSlaves.clear(); + _protoSlaves.clear(); + while (!_forwardClients.isEmpty()) + delete _forwardClients.takeFirst(); + + // build new one + const QJsonObject &obj = config.object(); + if ( !obj["json"].isNull() ) + { + const QJsonArray & addr = obj["json"].toArray(); + for (const auto& entry : addr) + { + addJsonSlave(entry.toString()); + } + } + + if ( !obj["proto"].isNull() ) + { + const QJsonArray & addr = obj["proto"].toArray(); + for (const auto& entry : addr) + { + addProtoSlave(entry.toString()); + } + } + + if (!_jsonSlaves.isEmpty() && obj["enable"].toBool() && _forwarder_enabled) + { + InfoIf(obj["enable"].toBool(true), _log, "Forward now to json targets '%s'", QSTRING_CSTR(_jsonSlaves.join(", "))); + connect(_hyperion, &Hyperion::forwardJsonMessage, this, &MessageForwarder::forwardJsonMessage, Qt::UniqueConnection); + } else if (_jsonSlaves.isEmpty() || ! obj["enable"].toBool() || !_forwarder_enabled) + disconnect(_hyperion, &Hyperion::forwardJsonMessage, 0, 0); + + if (!_protoSlaves.isEmpty() && obj["enable"].toBool() && _forwarder_enabled) + { + InfoIf(obj["enable"].toBool(true), _log, "Forward now to proto targets '%s'", QSTRING_CSTR(_protoSlaves.join(", "))); + connect(_hyperion, &Hyperion::forwardProtoMessage, this, &MessageForwarder::forwardProtoMessage, Qt::UniqueConnection); + } else if ( _protoSlaves.isEmpty() || ! obj["enable"].toBool() || !_forwarder_enabled) + disconnect(_hyperion, &Hyperion::forwardProtoMessage, 0, 0); + + // update comp state + _hyperion->getComponentRegister().componentStateChanged(hyperion::COMP_FORWARDER, obj["enable"].toBool(true)); + } +} + +void MessageForwarder::componentStateChanged(const hyperion::Components component, bool enable) +{ + if (component == hyperion::COMP_FORWARDER && _forwarder_enabled != enable) + { + _forwarder_enabled = enable; + handleSettingsUpdate(settings::NETFORWARD, _hyperion->getSetting(settings::NETFORWARD)); + Info(_log, "Forwarder change state to %s", (_forwarder_enabled ? "enabled" : "disabled")); + _hyperion->getComponentRegister().componentStateChanged(component, _forwarder_enabled); + } +} + +void MessageForwarder::handlePriorityChanges(const quint8 &priority) +{ + const QJsonObject obj = _hyperion->getSetting(settings::NETFORWARD).object(); + if (priority != 0 && _forwarder_enabled && obj["enable"].toBool()) + { + _protoSlaves.clear(); + while (!_forwardClients.isEmpty()) + delete _forwardClients.takeFirst(); + + hyperion::Components activePrio = _hyperion->getPriorityInfo(priority).componentId; + if (activePrio == hyperion::COMP_GRABBER || activePrio == hyperion::COMP_V4L) + { + if ( !obj["proto"].isNull() ) + { + const QJsonArray & addr = obj["proto"].toArray(); + for (const auto& entry : addr) + { + addProtoSlave(entry.toString()); + } + } + connect(_hyperion, &Hyperion::forwardProtoMessage, this, &MessageForwarder::forwardProtoMessage, Qt::UniqueConnection); + } + else + disconnect(_hyperion, &Hyperion::forwardProtoMessage, 0, 0); + } +} void MessageForwarder::addJsonSlave(QString slave) { QStringList parts = slave.split(":"); if (parts.size() != 2) - throw std::runtime_error(QString("HYPERION (forwarder) ERROR: Wrong address: unable to parse address (%1)").arg(slave).toStdString()); + { + Error(_log, "Unable to parse address (%s)",QSTRING_CSTR(slave)); + return; + } bool ok; - quint16 port = parts[1].toUShort(&ok); + parts[1].toUShort(&ok); if (!ok) - throw std::runtime_error(QString("HYPERION (forwarder) ERROR: Wrong address: Unable to parse the port number (%1)").arg(parts[1]).toStdString()); + { + Error(_log, "Unable to parse port number (%s)",QSTRING_CSTR(parts[1])); + return; + } - JsonSlaveAddress c; - c.addr = QHostAddress(parts[0]); - c.port = port; - _jsonSlaves << c; + // verify loop with jsonserver + const QJsonObject &obj = _hyperion->getSetting(settings::JSONSERVER).object(); + if(QHostAddress(parts[0]) == QHostAddress::LocalHost && parts[1].toInt() == obj["port"].toInt()) + { + Error(_log, "Loop between JsonServer and Forwarder! (%s)",QSTRING_CSTR(slave)); + return; + } + + if (_forwarder_enabled) + _jsonSlaves << slave; } void MessageForwarder::addProtoSlave(QString slave) { - _protoSlaves << slave; + QStringList parts = slave.split(":"); + if (parts.size() != 2) + { + Error(_log, "Unable to parse address (%s)",QSTRING_CSTR(slave)); + return; + } + + bool ok; + parts[1].toUShort(&ok); + if (!ok) + { + Error(_log, "Unable to parse port number (%s)",QSTRING_CSTR(parts[1])); + return; + } + + // verify loop with protoserver + const QJsonObject &obj = _hyperion->getSetting(settings::FLATBUFSERVER).object(); + if(QHostAddress(parts[0]) == QHostAddress::LocalHost && parts[1].toInt() == obj["port"].toInt()) + { + Error(_log, "Loop between ProtoServer and Forwarder! (%s)",QSTRING_CSTR(slave)); + return; + } + + if (_forwarder_enabled) + { + _protoSlaves << slave; + FlatBufferConnection* flatbuf = new FlatBufferConnection("Forwarder", slave.toLocal8Bit().constData(), _priority, false); + _forwardClients << flatbuf; + } } -QStringList MessageForwarder::getProtoSlaves() +void MessageForwarder::forwardJsonMessage(const QJsonObject &message) { - return _protoSlaves; + if (_forwarder_enabled) + { + QTcpSocket client; + for (int i=0; i<_jsonSlaves.size(); i++) + { + QStringList parts = _jsonSlaves.at(i).split(":"); + client.connectToHost(QHostAddress(parts[0]), parts[1].toUShort()); + if ( client.waitForConnected(500) ) + { + sendJsonMessage(message,&client); + client.close(); + } + } + } } -QList MessageForwarder::getJsonSlaves() +void MessageForwarder::forwardProtoMessage(const Image &image) { - return _jsonSlaves; + if (_forwarder_enabled) + { + for (int i=0; i < _forwardClients.size(); i++) + _forwardClients.at(i)->setImage(image); + } } -bool MessageForwarder::protoForwardingEnabled() +void MessageForwarder::sendJsonMessage(const QJsonObject &message, QTcpSocket *socket) { - return ! _protoSlaves.empty(); -} + // for hyperion classic compatibility + QJsonObject jsonMessage = message; + if (jsonMessage.contains("tan") && jsonMessage["tan"].isNull()) + jsonMessage["tan"] = 100; -bool MessageForwarder::jsonForwardingEnabled() -{ - return ! _jsonSlaves.empty(); + // serialize message + QJsonDocument writer(jsonMessage); + QByteArray serializedMessage = writer.toJson(QJsonDocument::Compact) + "\n"; + + // write message + socket->write(serializedMessage); + if (!socket->waitForBytesWritten()) + { + Debug(_log, "Error while writing data to host"); + return; + } + + // read reply data + QByteArray serializedReply; + while (!serializedReply.contains('\n')) + { + // receive reply + if (!socket->waitForReadyRead()) + { + Debug(_log, "Error while writing data from host"); + return; + } + + serializedReply += socket->readAll(); + } + + // parse reply data + QJsonParseError error; + QJsonDocument reply = QJsonDocument::fromJson(serializedReply ,&error); + + if (error.error != QJsonParseError::NoError) + { + Error(_log, "Error while parsing reply: invalid json"); + return; + } } diff --git a/libsrc/hyperion/MultiColorAdjustment.cpp b/libsrc/hyperion/MultiColorAdjustment.cpp index 32985dba..1c5ff16c 100644 --- a/libsrc/hyperion/MultiColorAdjustment.cpp +++ b/libsrc/hyperion/MultiColorAdjustment.cpp @@ -1,10 +1,10 @@ // Hyperion includes #include -#include "MultiColorAdjustment.h" +#include MultiColorAdjustment::MultiColorAdjustment(const unsigned ledCnt) : _ledAdjustments(ledCnt, nullptr) - , _log(Logger::getInstance("ColorAdjust")) + , _log(Logger::getInstance("ADJUSTMENT")) { } @@ -23,10 +23,20 @@ void MultiColorAdjustment::addAdjustment(ColorAdjustment * adjustment) _adjustment.push_back(adjustment); } -void MultiColorAdjustment::setAdjustmentForLed(const QString& id, const unsigned startLed, const unsigned endLed) +void MultiColorAdjustment::setAdjustmentForLed(const QString& id, const unsigned startLed, unsigned endLed) { - Q_ASSERT(startLed <= endLed); - Q_ASSERT(endLed < _ledAdjustments.size()); + // abort + if(startLed >= endLed) + { + Error(_log,"startLed >= endLed -> %d >= %d", startLed, endLed); + return; + } + // catch wrong values + if(endLed > _ledAdjustments.size()) + { + Warning(_log,"The color calibration 'LED index' field has leds specified which aren't part of your led layout"); + endLed = _ledAdjustments.size(); + } // Get the identified adjustment (don't care if is nullptr) ColorAdjustment * adjustment = getAdjustment(id); @@ -38,17 +48,18 @@ void MultiColorAdjustment::setAdjustmentForLed(const QString& id, const unsigned bool MultiColorAdjustment::verifyAdjustments() const { + bool ok = true; for (unsigned iLed=0; iLed<_ledAdjustments.size(); ++iLed) { ColorAdjustment * adjustment = _ledAdjustments[iLed]; if (adjustment == nullptr) { - Error(_log, "No adjustment set for %d", iLed); - return false; + Warning(_log, "No calibration set for led %d", iLed); + ok = false; } } - return true; + return ok; } const QStringList & MultiColorAdjustment::getAdjustmentIds() @@ -96,7 +107,7 @@ void MultiColorAdjustment::applyAdjustment(std::vector& ledColors) uint8_t ogreen = color.green; uint8_t oblue = color.blue; uint8_t B_RGB, B_CMY, B_W; - + adjustment->_rgbTransform.transform(ored,ogreen,oblue); adjustment->_rgbTransform.getBrightnessComponents(B_RGB, B_CMY, B_W); @@ -104,7 +115,7 @@ void MultiColorAdjustment::applyAdjustment(std::vector& ledColors) uint32_t rng = (uint32_t) (ored) *(255-ogreen); uint32_t nrg = (uint32_t) (255-ored)*(ogreen); uint32_t rg = (uint32_t) (ored) *(ogreen); - + uint8_t black = nrng*(255-oblue)/65025; uint8_t red = rng *(255-oblue)/65025; uint8_t green = nrg *(255-oblue)/65025; @@ -113,7 +124,7 @@ void MultiColorAdjustment::applyAdjustment(std::vector& ledColors) uint8_t magenta = rng *(oblue) /65025; uint8_t yellow = rg *(255-oblue)/65025; uint8_t white = rg *(oblue) /65025; - + uint8_t OR, OG, OB, RR, RG, RB, GR, GG, GB, BR, BG, BB; uint8_t CR, CG, CB, MR, MG, MB, YR, YG, YB, WR, WG, WB; diff --git a/libsrc/hyperion/PriorityMuxer.cpp b/libsrc/hyperion/PriorityMuxer.cpp index d8443b92..b22a1800 100644 --- a/libsrc/hyperion/PriorityMuxer.cpp +++ b/libsrc/hyperion/PriorityMuxer.cpp @@ -1,39 +1,111 @@ // STL includes #include -#include #include +// qt incl +#include +#include + // Hyperion includes #include +// utils +#include + const int PriorityMuxer::LOWEST_PRIORITY = std::numeric_limits::max(); PriorityMuxer::PriorityMuxer(int ledCount) - : _currentPriority(PriorityMuxer::LOWEST_PRIORITY) + : QObject() + , _log(Logger::getInstance("HYPERION")) + , _currentPriority(PriorityMuxer::LOWEST_PRIORITY) + , _manualSelectedPriority(256) , _activeInputs() , _lowestPriorityInfo() + , _sourceAutoSelectEnabled(true) + , _updateTimer(new QTimer(this)) + , _timer(new QTimer(this)) + , _blockTimer(new QTimer(this)) { + // init lowest priority info _lowestPriorityInfo.priority = PriorityMuxer::LOWEST_PRIORITY; - _lowestPriorityInfo.timeoutTime_ms = 0; + _lowestPriorityInfo.timeoutTime_ms = -1; _lowestPriorityInfo.ledColors = std::vector(ledCount, {0, 0, 0}); _lowestPriorityInfo.componentId = hyperion::COMP_COLOR; _lowestPriorityInfo.origin = "System"; + _lowestPriorityInfo.owner = ""; - _activeInputs[_currentPriority] = _lowestPriorityInfo; + _activeInputs[PriorityMuxer::LOWEST_PRIORITY] = _lowestPriorityInfo; - // do a reuqest after blocking timer runs out - connect(&_timer, SIGNAL(timeout()), this, SLOT(emitReq())); - _timer.setSingleShot(true); - _blockTimer.setSingleShot(true); + // adapt to 1s interval for COLOR and EFFECT timeouts > -1 + connect(_timer, &QTimer::timeout, this, &PriorityMuxer::timeTrigger); + _timer->setSingleShot(true); + _blockTimer->setSingleShot(true); + // forward timeRunner signal to prioritiesChanged signal & threading workaround + connect(this, &PriorityMuxer::timeRunner, this, &PriorityMuxer::prioritiesChanged); + connect(this, &PriorityMuxer::signalTimeTrigger, this, &PriorityMuxer::timeTrigger); + + // start muxer timer + connect(_updateTimer, &QTimer::timeout, this, &PriorityMuxer::setCurrentTime); + _updateTimer->setInterval(250); + _updateTimer->start(); + InputInfo ninfo; } PriorityMuxer::~PriorityMuxer() { } -int PriorityMuxer::getCurrentPriority() const +void PriorityMuxer::setEnable(const bool& enable) { - return _currentPriority; + enable ? _updateTimer->start() : _updateTimer->stop(); +} + +bool PriorityMuxer::setSourceAutoSelectEnabled(const bool& enable, const bool& update) +{ + if(_sourceAutoSelectEnabled != enable) + { + // on disable we need to make sure the last priority call to setPriority is still valid + if(!enable && !_activeInputs.contains(_manualSelectedPriority)) + { + Warning(_log, "Can't disable auto selection, as the last manual selected priority (%d) is no longer available", _manualSelectedPriority); + return false; + } + + _sourceAutoSelectEnabled = enable; + Debug(_log, "Source auto select is now %s", enable ? "enabled" : "disabled"); + + // update _currentPriority if called from external + if(update) + setCurrentTime(); + + emit autoSelectChanged(enable); + return true; + } + return false; +} + +bool PriorityMuxer::setPriority(const uint8_t priority) +{ + if(_activeInputs.contains(priority)) + { + _manualSelectedPriority = priority; + // update auto select state -> update _currentPriority + setSourceAutoSelectEnabled(false); + return true; + } + return false; +} + +void PriorityMuxer::updateLedColorsLength(const int& ledCount) +{ + for (auto infoIt = _activeInputs.begin(); infoIt != _activeInputs.end();) + { + if (infoIt->ledColors.size() >= 1) + { + infoIt->ledColors.resize(ledCount, infoIt->ledColors.at(0)); + } + ++infoIt; + } } QList PriorityMuxer::getPriorities() const @@ -46,7 +118,7 @@ bool PriorityMuxer::hasPriority(const int priority) const return (priority == PriorityMuxer::LOWEST_PRIORITY) ? true : _activeInputs.contains(priority); } -const PriorityMuxer::InputInfo& PriorityMuxer::getInputInfo(const int priority) const +const PriorityMuxer::InputInfo PriorityMuxer::getInputInfo(const int priority) const { auto elemIt = _activeInputs.find(priority); if (elemIt == _activeInputs.end()) @@ -54,35 +126,133 @@ const PriorityMuxer::InputInfo& PriorityMuxer::getInputInfo(const int priority) elemIt = _activeInputs.find(PriorityMuxer::LOWEST_PRIORITY); if (elemIt == _activeInputs.end()) { - throw std::runtime_error("HYPERION (prioritymuxer) ERROR: no such priority"); + // fallback + return _lowestPriorityInfo; } } return elemIt.value(); } -void PriorityMuxer::setInput(const int priority, const std::vector& ledColors, const int64_t timeoutTime_ms, hyperion::Components component, const QString origin, unsigned smooth_cfg) +void PriorityMuxer::registerInput(const int priority, const hyperion::Components& component, const QString& origin, const QString& owner, unsigned smooth_cfg) { + // detect new registers + bool newInput = false; + if(!_activeInputs.contains(priority)) + newInput = true; + InputInfo& input = _activeInputs[priority]; input.priority = priority; - input.timeoutTime_ms = timeoutTime_ms; - input.ledColors = ledColors; + input.timeoutTime_ms = newInput ? -100 : input.timeoutTime_ms; input.componentId = component; input.origin = origin; input.smooth_cfg = smooth_cfg; - _currentPriority = qMin(_currentPriority, priority); + input.owner = owner; + + if(newInput) + { + Debug(_log,"Register new input '%s/%s' with priority %d as inactive", QSTRING_CSTR(origin), hyperion::componentToIdString(component), priority); + emit priorityChanged(priority, true); + emit prioritiesChanged(); + return; + } } -void PriorityMuxer::clearInput(const int priority) +const bool PriorityMuxer::setInput(const int priority, const std::vector& ledColors, int64_t timeout_ms) { - if (priority < PriorityMuxer::LOWEST_PRIORITY) + if(!_activeInputs.contains(priority)) { - _activeInputs.remove(priority); - if (_currentPriority == priority) - { - QList keys = _activeInputs.keys(); - _currentPriority = *std::min_element(keys.begin(), keys.end()); - } + Error(_log,"setInput() used without registerInput() for priority '%d', probably the priority reached timeout",priority); + return false; } + + // calc final timeout + if(timeout_ms > 0) + timeout_ms = QDateTime::currentMSecsSinceEpoch() + timeout_ms; + + InputInfo& input = _activeInputs[priority]; + // detect active <-> inactive changes + bool activeChange = false; + bool active = true; + if(input.timeoutTime_ms == -100 && timeout_ms != -100) + { + activeChange = true; + } + else if(timeout_ms == -100 && input.timeoutTime_ms != -100) + { + active = false; + activeChange = true; + } + // update input + input.timeoutTime_ms = timeout_ms; + input.ledColors = ledColors; + + // emit active change + if(activeChange) + { + Debug(_log, "Priority %d is now %s", priority, active ? "active" : "inactive"); + emit activeStateChanged(priority, active); + setCurrentTime(); + } + return true; +} + +const bool PriorityMuxer::setInputImage(const int priority, const Image& image, int64_t timeout_ms) +{ + if(!_activeInputs.contains(priority)) + { + Error(_log,"setInputImage() used without registerInput() for priority '%d', probably the priority reached timeout",priority); + return false; + } + + // calc final timeout + if(timeout_ms > 0) + timeout_ms = QDateTime::currentMSecsSinceEpoch() + timeout_ms; + + InputInfo& input = _activeInputs[priority]; + // detect active <-> inactive changes + bool activeChange = false; + bool active = true; + if(input.timeoutTime_ms == -100 && timeout_ms != -100) + { + activeChange = true; + } + else if(timeout_ms == -100 && input.timeoutTime_ms != -100) + { + active = false; + activeChange = true; + } + // update input + input.timeoutTime_ms = timeout_ms; + input.image = image; + + // emit active change + if(activeChange) + { + Debug(_log, "Priority %d is now %s", priority, active ? "active" : "inactive"); + emit activeStateChanged(priority, active); + setCurrentTime(); + } + return true; +} + +const bool PriorityMuxer::setInputInactive(const quint8& priority) +{ + Image image; + return setInputImage(priority, image, -100); +} + +const bool PriorityMuxer::clearInput(const uint8_t priority) +{ + if (priority < PriorityMuxer::LOWEST_PRIORITY && _activeInputs.remove(priority)) + { + Debug(_log,"Removed source priority %d",priority); + // on clear success update _currentPriority + setCurrentTime(); + emit priorityChanged(priority, false); + emit prioritiesChanged(); + return true; + } + return false; } void PriorityMuxer::clearAll(bool forceClearAll) @@ -97,47 +267,81 @@ void PriorityMuxer::clearAll(bool forceClearAll) { for(auto key : _activeInputs.keys()) { - if (key < PriorityMuxer::LOWEST_PRIORITY-1) + const InputInfo info = getInputInfo(key); + if ((info.componentId == hyperion::COMP_COLOR || info.componentId == hyperion::COMP_EFFECT) && key < PriorityMuxer::LOWEST_PRIORITY-1) { - _activeInputs.remove(key); + clearInput(key); } } } } -void PriorityMuxer::setCurrentTime(const int64_t& now) +void PriorityMuxer::setCurrentTime(void) { - _currentPriority = PriorityMuxer::LOWEST_PRIORITY; + const int64_t now = QDateTime::currentMSecsSinceEpoch(); + int newPriority = PriorityMuxer::LOWEST_PRIORITY; for (auto infoIt = _activeInputs.begin(); infoIt != _activeInputs.end();) { if (infoIt->timeoutTime_ms > 0 && infoIt->timeoutTime_ms <= now) { + quint8 tPrio = infoIt->priority; infoIt = _activeInputs.erase(infoIt); + Debug(_log,"Timeout clear for priority %d",tPrio); + emit priorityChanged(tPrio, false); + emit prioritiesChanged(); } else { - _currentPriority = qMin(_currentPriority, infoIt->priority); - - // call emitReq when effect or color is running with timeout > -1, blacklist prio 255 - if(infoIt->priority < 254 && infoIt->timeoutTime_ms > -1 && (infoIt->componentId == hyperion::COMP_EFFECT || infoIt->componentId == hyperion::COMP_COLOR)) - { - emitReq(); - } + // timeoutTime of -100 is awaiting data (inactive); skip + if(infoIt->timeoutTime_ms >= -100) + newPriority = qMin(newPriority, infoIt->priority); + + // call timeTrigger when effect or color is running with timeout > 0, blacklist prio 255 + if(infoIt->priority < 254 && infoIt->timeoutTime_ms > 0 && (infoIt->componentId == hyperion::COMP_EFFECT || infoIt->componentId == hyperion::COMP_COLOR)) + emit signalTimeTrigger(); // as signal to prevent Threading issues + ++infoIt; } } + // eval if manual selected prio is still available + if(!_sourceAutoSelectEnabled) + { + if(_activeInputs.contains(_manualSelectedPriority)) + { + newPriority = _manualSelectedPriority; + } + else + { + Debug(_log, "The manual selected priority '%d' is no longer available, switching to auto selection", _manualSelectedPriority); + // update state, but no _currentPriority re-eval + setSourceAutoSelectEnabled(true, false); + } + } + // apply & emit on change (after apply!) + bool changed = false; + if(_currentPriority != newPriority) + changed = true; + + _currentPriority = newPriority; + + if(changed) + { + Debug(_log, "Set visible priority to %d", newPriority); + emit visiblePriorityChanged(newPriority); + emit prioritiesChanged(); + } } -void PriorityMuxer::emitReq() +void PriorityMuxer::timeTrigger() { - if(_blockTimer.isActive()) + if(_blockTimer->isActive()) { - _timer.start(500); + _timer->start(500); } else { - emit timerunner(); - _blockTimer.start(1000); + emit timeRunner(); + _blockTimer->start(1000); } } diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp new file mode 100644 index 00000000..e024aa1b --- /dev/null +++ b/libsrc/hyperion/SettingsManager.cpp @@ -0,0 +1,190 @@ +// proj +#include + +// util +#include + +// json schema process +#include +#include + +// write config to filesystem +#include + +// hyperion +#include + +QJsonObject SettingsManager::schemaJson; + +SettingsManager::SettingsManager(Hyperion* hyperion, const quint8& instance, const QString& configFile) + : _hyperion(hyperion) + , _log(Logger::getInstance("SettingsManager")) +{ + connect(this, &SettingsManager::settingsChanged, _hyperion, &Hyperion::settingsChanged); + // get schema + if(schemaJson.isEmpty()) + { + Q_INIT_RESOURCE(resource); + try + { + schemaJson = QJsonFactory::readSchema(":/hyperion-schema"); + } + catch(const std::runtime_error& error) + { + throw std::runtime_error(error.what()); + } + } + // get default config + QJsonObject defaultConfig; + if(!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log)) + throw std::runtime_error("Failed to read default config"); + + Info(_log, "Selected configuration file: %s", QSTRING_CSTR(configFile)); + QJsonSchemaChecker schemaCheckerT; + + if(!JsonUtils::readFile(configFile, _qconfig, _log)) + throw std::runtime_error("Failed to load config!"); + + // validate config with schema and correct it if required + QPair validate = schemaCheckerT.validate(_qconfig); + + // errors in schema syntax, abort + if (!validate.second) + { + foreach (auto & schemaError, schemaCheckerT.getMessages()) + Error(_log, "Schema Syntax Error: %s", QSTRING_CSTR(schemaError)); + + throw std::runtime_error("ERROR: Hyperion schema has syntax errors!"); + } + // errors in configuration, correct it! + if (!validate.first) + { + Warning(_log,"Errors have been found in the configuration file. Automatic correction has been applied"); + _qconfig = schemaCheckerT.getAutoCorrectedConfig(_qconfig); + + foreach (auto & schemaError, schemaCheckerT.getMessages()) + Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); + + if (!JsonUtils::write(configFile, _qconfig, _log)) + throw std::runtime_error("ERROR: Can't save configuration file, aborting"); + } + + Debug(_log,"Settings database initialized") +} + +SettingsManager::SettingsManager(const quint8& instance, const QString& configFile) + : _hyperion(nullptr) + , _log(Logger::getInstance("SettingsManager")) +{ + Q_INIT_RESOURCE(resource); + // get schema + if(schemaJson.isEmpty()) + { + try + { + schemaJson = QJsonFactory::readSchema(":/hyperion-schema"); + } + catch(const std::runtime_error& error) + { + throw std::runtime_error(error.what()); + } + } + // get default config + QJsonObject defaultConfig; + if(!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log)) + throw std::runtime_error("Failed to read default config"); + + Info(_log, "Selected configuration file: %s", QSTRING_CSTR(configFile)); + QJsonSchemaChecker schemaCheckerT; + + if(!JsonUtils::readFile(configFile, _qconfig, _log)) + throw std::runtime_error("Failed to load config!"); + + // validate config with schema and correct it if required + QPair validate = schemaCheckerT.validate(_qconfig); + + // errors in schema syntax, abort + if (!validate.second) + { + foreach (auto & schemaError, schemaCheckerT.getMessages()) + Error(_log, "Schema Syntax Error: %s", QSTRING_CSTR(schemaError)); + + throw std::runtime_error("ERROR: Hyperion schema has syntax errors!"); + } + // errors in configuration, correct it! + if (!validate.first) + { + Warning(_log,"Errors have been found in the configuration file. Automatic correction has been applied"); + _qconfig = schemaCheckerT.getAutoCorrectedConfig(_qconfig); + + foreach (auto & schemaError, schemaCheckerT.getMessages()) + Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); + + if (!JsonUtils::write(configFile, _qconfig, _log)) + throw std::runtime_error("ERROR: Can't save configuration file, aborting"); + } + + Debug(_log,"Settings database initialized") +} + +SettingsManager::~SettingsManager() +{ +} + +const QJsonDocument SettingsManager::getSetting(const settings::type& type) +{ + QString key = settings::typeToString(type); + if(_qconfig[key].isObject()) + return QJsonDocument(_qconfig[key].toObject()); + else + return QJsonDocument(_qconfig[key].toArray()); +} + +const bool SettingsManager::saveSettings(QJsonObject config, const bool& correct) +{ + // we need to validate data against schema + QJsonSchemaChecker schemaChecker; + schemaChecker.setSchema(schemaJson); + if (!schemaChecker.validate(config).first) + { + if(!correct) + { + Error(_log,"Failed to save configuration, errors during validation"); + return false; + } + Warning(_log,"Fixing json data!"); + config = schemaChecker.getAutoCorrectedConfig(config); + + foreach (auto & schemaError, schemaChecker.getMessages()) + Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); + } + + // save data to file + if(_hyperion != nullptr) + { + if(!JsonUtils::write(_hyperion->getConfigFilePath(), config, _log)) + return false; + } + + // compare old data with new data to emit/save changes accordingly + for(const auto key : config.keys()) + { + QString newData, oldData; + + _qconfig[key].isObject() + ? oldData = QString(QJsonDocument(_qconfig[key].toObject()).toJson(QJsonDocument::Compact)) + : oldData = QString(QJsonDocument(_qconfig[key].toArray()).toJson(QJsonDocument::Compact)); + + config[key].isObject() + ? newData = QString(QJsonDocument(config[key].toObject()).toJson(QJsonDocument::Compact)) + : newData = QString(QJsonDocument(config[key].toArray()).toJson(QJsonDocument::Compact)); + + if(oldData != newData) + emit settingsChanged(settings::stringToType(key), QJsonDocument::fromJson(newData.toLocal8Bit())); + } + + // store the current state + _qconfig = config; + + return true; +} diff --git a/libsrc/hyperion/hyperion-icon_32px.png b/libsrc/hyperion/hyperion-icon_32px.png deleted file mode 100644 index 12196277..00000000 Binary files a/libsrc/hyperion/hyperion-icon_32px.png and /dev/null differ diff --git a/libsrc/hyperion/hyperion.schema.json b/libsrc/hyperion/hyperion.schema.json index 501335f3..8450773f 100644 --- a/libsrc/hyperion/hyperion.schema.json +++ b/libsrc/hyperion/hyperion.schema.json @@ -51,9 +51,9 @@ { "$ref": "schema-jsonServer.json" }, - "protoServer" : + "flatbufServer": { - "$ref": "schema-protoServer.json" + "$ref": "schema-flatbufServer.json" }, "boblightServer" : { @@ -71,6 +71,10 @@ { "$ref": "schema-effects.json" }, + "instCapture": + { + "$ref": "schema-instCapture.json" + }, "ledConfig": { "$ref": "schema-ledConfig.json" diff --git a/libsrc/hyperion/resource.qrc b/libsrc/hyperion/resource.qrc index 8616fa79..2d8c1c36 100644 --- a/libsrc/hyperion/resource.qrc +++ b/libsrc/hyperion/resource.qrc @@ -1,6 +1,5 @@ - hyperion-icon_32px.png hyperion.schema.json ../../config/hyperion.config.json.default schema/schema-general.json @@ -15,12 +14,13 @@ schema/schema-backgroundEffect.json schema/schema-forwarder.json schema/schema-jsonServer.json - schema/schema-protoServer.json + schema/schema-flatbufServer.json schema/schema-boblightServer.json schema/schema-udpListener.json schema/schema-webConfig.json schema/schema-effects.json schema/schema-ledConfig.json schema/schema-leds.json + schema/schema-instCapture.json diff --git a/libsrc/hyperion/schema/schema-color.json b/libsrc/hyperion/schema/schema-color.json index fe8181d5..1bcb1705 100644 --- a/libsrc/hyperion/schema/schema-color.json +++ b/libsrc/hyperion/schema/schema-color.json @@ -46,22 +46,6 @@ "default" : "*", "propertyOrder" : 2 }, - "black" : - { - "type" : "array", - "title" : "edt_conf_color_black_title", - "format" : "colorpicker", - "required" : true, - "default": [0,0,0], - "items" : { - "type" : "integer", - "minimum" : 0, - "maximum" : 255 - }, - "minItems" : 3, - "maxItems" : 3, - "propertyOrder" : 3 - }, "white" : { "type" : "array", diff --git a/libsrc/hyperion/schema/schema-device.json b/libsrc/hyperion/schema/schema-device.json index 53556c79..9785f3d2 100644 --- a/libsrc/hyperion/schema/schema-device.json +++ b/libsrc/hyperion/schema/schema-device.json @@ -2,18 +2,21 @@ "type" : "object", "title" : "edt_dev_general_heading_title", "required" : true, - "defaultProperties": ["ledCount","colorOrder","rewriteTime","minimumWriteTime"], + "defaultProperties": ["hardwareLedCount","colorOrder","rewriteTime"], "properties" : { "type" : { - "type" : "string" + "type" : "string", + "propertyOrder" : 1 }, - "ledCount" : + "hardwareLedCount" : { "type" : "integer", - "minimum" : 0, - "title" : "edt_dev_general_ledCount_title", + "title" : "edt_dev_general_hardwareLedCount_title", + "minimum" : 1, + "default" : 1, + "access" : "expert", "propertyOrder" : 2 }, "colorOrder" : diff --git a/libsrc/hyperion/schema/schema-flatbufServer.json b/libsrc/hyperion/schema/schema-flatbufServer.json new file mode 100644 index 00000000..e86633d9 --- /dev/null +++ b/libsrc/hyperion/schema/schema-flatbufServer.json @@ -0,0 +1,37 @@ +{ + "type" : "object", + "required" : true, + "title" : "edt_conf_fbs_heading_title", + "properties" : + { + "enable" : + { + "type" : "boolean", + "required" : true, + "title" : "edt_conf_general_enable_title", + "default" : true, + "propertyOrder" : 1 + }, + "port" : + { + "type" : "integer", + "required" : true, + "title" : "edt_conf_general_port_title", + "minimum" : 1024, + "maximum" : 65535, + "default" : 19400, + "propertyOrder" : 2 + }, + "timeout" : + { + "type" : "integer", + "required" : true, + "title" : "edt_conf_fbs_timeout_title", + "append" : "edt_append_s", + "minimum" : 1, + "default" : 5, + "propertyOrder" : 3 + } + }, + "additionalProperties" : false +} diff --git a/libsrc/hyperion/schema/schema-framegrabber.json b/libsrc/hyperion/schema/schema-framegrabber.json index 366d05b6..8db55c78 100644 --- a/libsrc/hyperion/schema/schema-framegrabber.json +++ b/libsrc/hyperion/schema/schema-framegrabber.json @@ -3,18 +3,11 @@ "title" : "edt_conf_fg_heading_title", "properties" : { - "enable" : - { - "type" : "boolean", - "title" : "edt_conf_general_enable_title", - "default" : true, - "propertyOrder" : 1 - }, "type" : { "type" : "string", "title" : "edt_conf_fg_type_title", - "enum" : ["auto","dispmanx","amlogic","x11","framebuffer"], + "enum" : ["auto","dispmanx","amlogic","x11","framebuffer","qt"], "default" : "auto", "propertyOrder" : 2 }, @@ -45,15 +38,6 @@ "append" : "edt_append_hz", "propertyOrder" : 4 }, - "priority" : - { - "type" : "integer", - "title" : "edt_conf_general_priority_title", - "minimum" : 100, - "maximum" : 254, - "default" : 250, - "propertyOrder" : 5 - }, "cropLeft" : { "type" : "integer", @@ -90,42 +74,28 @@ "append" : "edt_append_pixel", "propertyOrder" : 9 }, - "useXGetImage" : + "pixelDecimation" : { - "type" : "boolean", - "title" : "edt_conf_fg_useXGetImage_title", - "default" : false, + "type" : "integer", + "title" : "edt_conf_fg_pixelDecimation_title", + "minimum" : 1, + "maximum" : 30, + "default" : 8, "propertyOrder" : 10 }, - "horizontalPixelDecimation" : - { - "type" : "integer", - "title" : "edt_conf_fg_horizontalPixelDecimation_title", - "minimum" : 0, - "default" : 8, - "propertyOrder" : 11 - }, - "verticalPixelDecimation" : - { - "type" : "integer", - "title" : "edt_conf_fg_verticalPixelDecimation_title", - "minimum" : 0, - "default" : 8, - "propertyOrder" : 12 - }, "device" : { "type" : "string", "title" : "edt_conf_fg_device_title", "default" : "/dev/fb0", - "propertyOrder" : 13 + "propertyOrder" : 11 }, "display" : { "type" : "integer", "title" : "edt_conf_fg_display_title", "minimum" : 0, - "propertyOrder" : 14 + "propertyOrder" : 12 } }, "additionalProperties" : false diff --git a/libsrc/hyperion/schema/schema-grabberV4L2.json b/libsrc/hyperion/schema/schema-grabberV4L2.json index e7356006..01df9d8d 100644 --- a/libsrc/hyperion/schema/schema-grabberV4L2.json +++ b/libsrc/hyperion/schema/schema-grabberV4L2.json @@ -3,7 +3,7 @@ "required" : true, "title" : "edt_conf_v4l2_heading_title", "minItems": 1, - "maxItems": 2, + "maxItems": 1, "items": { "type" : "object", @@ -11,90 +11,36 @@ "title" : "edt_conf_v4l2_heading_title", "properties" : { - "enable" : - { - "type" : "boolean", - "title" : "edt_conf_general_enable_title", - "default" : false, - "required" : true, - "propertyOrder" : 1 - }, "device" : { "type" : "string", "title" : "edt_conf_v4l2_device_title", "default" : "auto", + "minLength" : 4, "required" : true, - "propertyOrder" : 2 - }, - "input" : - { - "type" : "integer", - "title" : "edt_conf_v4l2_input_title", - "minimum" : 0, - "default" : 0, - "required" : true, - "propertyOrder" : 3 + "propertyOrder" : 1 }, "standard" : { "type" : "string", "title" : "edt_conf_v4l2_standard_title", - "enum" : ["PAL","NTSC","SECAM"], - "default" : "PAL", + "enum" : ["PAL","NTSC","SECAM","NO_CHANGE"], + "default" : "NO_CHANGE", "options" : { - "enum_titles" : ["edt_conf_enum_PAL", "edt_conf_enum_NTSC", "edt_conf_enum_SECAM"] + "enum_titles" : ["edt_conf_enum_PAL", "edt_conf_enum_NTSC", "edt_conf_enum_SECAM", "edt_conf_enum_NO_CHANGE"] }, "required" : true, - "propertyOrder" : 4 - }, - "width" : - { - "type" : "integer", - "title" : "edt_conf_v4l2_width_title", - "minimum" : 0, - "default" : 0, - "append" : "edt_append_pixel", - "required" : true, - "propertyOrder" : 5 - }, - "height" : - { - "type" : "integer", - "title" : "edt_conf_v4l2_height_title", - "minimum" : 0, - "default" : 0, - "append" : "edt_append_pixel", - "required" : true, - "propertyOrder" : 6 - }, - "frameDecimation" : - { - "type" : "integer", - "title" : "edt_conf_v4l2_frameDecimation_title", - "minimum" : 0, - "default" : 2, - "required" : true, - "propertyOrder" : 7 + "propertyOrder" : 2 }, "sizeDecimation" : { "type" : "integer", - "title" : "Size decimation", - "minimum" : 0, + "title" : "edt_conf_v4l2_sizeDecimation_title", + "minimum" : 1, + "maximum" : 30, "default" : 6, "required" : true, - "propertyOrder" : 8 - }, - "priority" : - { - "type" : "integer", - "minimum" : 100, - "maximum" : 253, - "title" : "edt_conf_general_priority_title", - "default" : 240, - "required" : true, - "propertyOrder" : 9 + "propertyOrder" : 3 }, "cropLeft" : { @@ -104,7 +50,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 11 + "propertyOrder" : 4 }, "cropRight" : { @@ -114,7 +60,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 12 + "propertyOrder" : 5 }, "cropTop" : { @@ -124,7 +70,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 13 + "propertyOrder" : 6 }, "cropBottom" : { @@ -134,7 +80,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 14 + "propertyOrder" : 7 }, "signalDetection" : { @@ -142,7 +88,7 @@ "title" : "edt_conf_v4l2_signalDetection_title", "default" : false, "required" : true, - "propertyOrder" : 15 + "propertyOrder" : 8 }, "redSignalThreshold" : { @@ -158,7 +104,7 @@ } }, "required" : true, - "propertyOrder" : 16 + "propertyOrder" : 9 }, "greenSignalThreshold" : { @@ -174,7 +120,7 @@ } }, "required" : true, - "propertyOrder" : 17 + "propertyOrder" : 10 }, "blueSignalThreshold" : { @@ -190,7 +136,7 @@ } }, "required" : true, - "propertyOrder" : 18 + "propertyOrder" : 11 }, "sDVOffsetMin" : { @@ -206,7 +152,7 @@ } }, "required" : true, - "propertyOrder" : 19 + "propertyOrder" : 12 }, "sDVOffsetMax" : { @@ -222,7 +168,7 @@ } }, "required" : true, - "propertyOrder" : 20 + "propertyOrder" : 13 }, "sDHOffsetMin" : { @@ -238,7 +184,7 @@ } }, "required" : true, - "propertyOrder" : 21 + "propertyOrder" : 14 }, "sDHOffsetMax" : { @@ -254,7 +200,7 @@ } }, "required" : true, - "propertyOrder" : 22 + "propertyOrder" : 15 } }, "additionalProperties" : false diff --git a/libsrc/hyperion/schema/schema-instCapture.json b/libsrc/hyperion/schema/schema-instCapture.json new file mode 100644 index 00000000..608717c1 --- /dev/null +++ b/libsrc/hyperion/schema/schema-instCapture.json @@ -0,0 +1,45 @@ +{ + "type" : "object", + "required" : true, + "title" : "edt_conf_instC_heading_title", + "properties" : + { + "systemEnable" : + { + "type" : "boolean", + "required" : true, + "title" : "edt_conf_instC_systemEnable_title", + "default" : true, + "propertyOrder" : 1 + }, + "systemPriority" : + { + "type" : "integer", + "required" : true, + "title" : "edt_conf_general_priority_title", + "minimum" : 100, + "maximum" : 253, + "default" : 250, + "propertyOrder" : 2 + }, + "v4lEnable" : + { + "type" : "boolean", + "required" : true, + "title" : "edt_conf_instC_v4lEnable_title", + "default" : false, + "propertyOrder" : 3 + }, + "v4lPriority" : + { + "type" : "integer", + "required" : true, + "title" : "edt_conf_general_priority_title", + "minimum" : 100, + "maximum" : 253, + "default" : 240, + "propertyOrder" : 4 + } + }, + "additionalProperties" : false +} diff --git a/libsrc/hyperion/schema/schema-protoServer.json b/libsrc/hyperion/schema/schema-protoServer.json deleted file mode 100644 index a344ba06..00000000 --- a/libsrc/hyperion/schema/schema-protoServer.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type" : "object", - "required" : true, - "title" : "edt_conf_ps_heading_title", - "properties" : - { - "port" : - { - "type" : "integer", - "required" : true, - "title" : "edt_conf_general_port_title", - "minimum" : 1024, - "maximum" : 65535, - "default" : 19445 - } - }, - "additionalProperties" : false -} diff --git a/libsrc/hyperion/schema/schema-smoothing.json b/libsrc/hyperion/schema/schema-smoothing.json index bf7d6e29..9e3f64ad 100644 --- a/libsrc/hyperion/schema/schema-smoothing.json +++ b/libsrc/hyperion/schema/schema-smoothing.json @@ -17,7 +17,8 @@ "enum" : ["linear"], "default" : "linear", "options" : { - "enum_titles" : ["edt_conf_enum_linear"] + "enum_titles" : ["edt_conf_enum_linear"], + "hidden":true }, "propertyOrder" : 2 }, diff --git a/libsrc/hyperion/schema/schema-webConfig.json b/libsrc/hyperion/schema/schema-webConfig.json index 6440dc76..ea33a383 100644 --- a/libsrc/hyperion/schema/schema-webConfig.json +++ b/libsrc/hyperion/schema/schema-webConfig.json @@ -3,14 +3,6 @@ "title" : "edt_conf_webc_heading_title", "properties" : { - "enable" : - { - "type" : "boolean", - "title" : "edt_conf_general_enable_title", - "default" : true, - "access" : "expert", - "propertyOrder" : 1 - }, "document_root" : { "type" : "string", diff --git a/libsrc/jsonserver/CMakeLists.txt b/libsrc/jsonserver/CMakeLists.txt index dfddffa5..1f0bc310 100644 --- a/libsrc/jsonserver/CMakeLists.txt +++ b/libsrc/jsonserver/CMakeLists.txt @@ -8,6 +8,7 @@ FILE ( GLOB JsonServer_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DI add_library(jsonserver ${JsonServer_SOURCES} ) target_link_libraries(jsonserver + hyperion-api hyperion Qt5::Network Qt5::Gui diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index 554361b2..692de9a9 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -1,40 +1,63 @@ // project includes #include "JsonClientConnection.h" -#include +#include + +// qt inc #include +#include + +// websocket includes +#include "webserver/WebSocketClient.h" JsonClientConnection::JsonClientConnection(QTcpSocket *socket) : QObject() , _socket(socket) + , _websocketClient(nullptr) , _receiveBuffer() , _log(Logger::getInstance("JSONCLIENTCONNECTION")) { connect(_socket, &QTcpSocket::disconnected, this, &JsonClientConnection::disconnected); connect(_socket, &QTcpSocket::readyRead, this, &JsonClientConnection::readRequest); - // create a new instance of JsonProcessor - _jsonProcessor = new JsonProcessor(socket->peerAddress().toString(), _log, this); - // get the callback messages from JsonProcessor and send it to the client - connect(_jsonProcessor,SIGNAL(callbackMessage(QJsonObject)),this,SLOT(sendMessage(QJsonObject))); + // create a new instance of JsonAPI + _jsonAPI = new JsonAPI(socket->peerAddress().toString(), _log, this); + // get the callback messages from JsonAPI and send it to the client + connect(_jsonAPI,SIGNAL(callbackMessage(QJsonObject)),this,SLOT(sendMessage(QJsonObject))); } void JsonClientConnection::readRequest() { _receiveBuffer += _socket->readAll(); - // raw socket data, handling as usual - int bytes = _receiveBuffer.indexOf('\n') + 1; - while(bytes > 0) + + // might be an old hyperion classic handshake request or raw socket data + if(_receiveBuffer.contains("Upgrade: websocket")) { - // create message string - QString message(QByteArray(_receiveBuffer.data(), bytes)); + if(_websocketClient == Q_NULLPTR) + { + // disconnect this slot from socket for further requests + disconnect(_socket, &QTcpSocket::readyRead, this, &JsonClientConnection::readRequest); + int start = _receiveBuffer.indexOf("Sec-WebSocket-Key") + 19; + QByteArray header(_receiveBuffer.mid(start, _receiveBuffer.indexOf("\r\n", start) - start).data()); + _websocketClient = new WebSocketClient(header, _socket, this); + } + } + else + { + // raw socket data, handling as usual + int bytes = _receiveBuffer.indexOf('\n') + 1; + while(bytes > 0) + { + // create message string + QString message(QByteArray(_receiveBuffer.data(), bytes)); - // remove message data from buffer - _receiveBuffer = _receiveBuffer.mid(bytes); + // remove message data from buffer + _receiveBuffer = _receiveBuffer.mid(bytes); - // handle message - _jsonProcessor->handleMessage(message); + // handle message + _jsonAPI->handleMessage(message); - // try too look up '\n' again - bytes = _receiveBuffer.indexOf('\n') + 1; + // try too look up '\n' again + bytes = _receiveBuffer.indexOf('\n') + 1; + } } } diff --git a/libsrc/jsonserver/JsonClientConnection.h b/libsrc/jsonserver/JsonClientConnection.h index 1195e7c5..0434bc5d 100644 --- a/libsrc/jsonserver/JsonClientConnection.h +++ b/libsrc/jsonserver/JsonClientConnection.h @@ -3,15 +3,17 @@ // Qt includes #include #include +#include // util includes #include -class JsonProcessor; +class JsonAPI; class QTcpSocket; +class WebSocketClient; /// -/// The Connection object created by \a JsonServer when a new connection is establshed +/// The Connection object created by \a JsonServer when a new connection is established /// class JsonClientConnection : public QObject { @@ -40,8 +42,9 @@ private slots: private: QTcpSocket* _socket; - /// new instance of JsonProcessor - JsonProcessor * _jsonProcessor; + WebSocketClient* _websocketClient; + /// new instance of JsonAPI + JsonAPI * _jsonAPI; /// The buffer used for reading data from the socket QByteArray _receiveBuffer; diff --git a/libsrc/jsonserver/JsonServer.cpp b/libsrc/jsonserver/JsonServer.cpp index 3c8e9e73..4b671d87 100644 --- a/libsrc/jsonserver/JsonServer.cpp +++ b/libsrc/jsonserver/JsonServer.cpp @@ -5,42 +5,28 @@ #include #include "JsonClientConnection.h" -// hyperion include -#include +// bonjour include +#include // qt includes +#include #include #include #include -JsonServer::JsonServer(uint16_t port) +JsonServer::JsonServer(const QJsonDocument& config) : QObject() - , _server() - , _hyperion(Hyperion::getInstance()) + , _server(new QTcpServer(this)) , _openConnections() , _log(Logger::getInstance("JSONSERVER")) { - if (!_server.listen(QHostAddress::Any, port)) - { - throw std::runtime_error("JSONSERVER ERROR: could not bind to port"); - } - - QList list = Hyperion::getInstance()->getForwarder()->getJsonSlaves(); - for ( int i=0; iisListening()) + return; + + if (!_server->listen(QHostAddress::Any, _port)) + { + Error(_log,"Could not bind to port '%d', please use an available port", _port); + return; + } + Info(_log, "Started on port %d", _port); + + if(_serviceRegister == nullptr) + { + _serviceRegister = new BonjourServiceRegister(this); + _serviceRegister->registerService("_hyperiond-json._tcp", _port); + } + else if( _serviceRegister->getPort() != _port) + { + delete _serviceRegister; + _serviceRegister = new BonjourServiceRegister(this); + _serviceRegister->registerService("_hyperiond-json._tcp", _port); + } +} + +void JsonServer::stop() +{ + if(!_server->isListening()) + return; + + _server->close(); + Info(_log, "Stopped"); +} + +void JsonServer::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::JSONSERVER) + { + QJsonObject obj = config.object(); + if(_port != obj["port"].toInt()) + { + _port = obj["port"].toInt(); + stop(); + start(); + } + } +} + uint16_t JsonServer::getPort() const { - return _server.serverPort(); + return _port; } void JsonServer::newConnection() { - while(_server.hasPendingConnections()) + while(_server->hasPendingConnections()) { - if (QTcpSocket * socket = _server.nextPendingConnection()) + if (QTcpSocket * socket = _server->nextPendingConnection()) { Debug(_log, "New connection from: %s ",socket->localAddress().toString().toStdString().c_str()); JsonClientConnection * connection = new JsonClientConnection(socket); @@ -80,76 +114,3 @@ void JsonServer::closedConnection(void) // schedule to delete the connection object connection->deleteLater(); } - -void JsonServer::componentStateChanged(const hyperion::Components component, bool enable) -{ - if (component == hyperion::COMP_FORWARDER && _forwarder_enabled != enable) - { - _forwarder_enabled = enable; - Info(_log, "forwarder change state to %s", (enable ? "enabled" : "disabled") ); - if(_forwarder_enabled) - { - connect(_hyperion, &Hyperion::forwardJsonMessage, this, &JsonServer::forwardJsonMessage); - } - else - { - disconnect(_hyperion, &Hyperion::forwardJsonMessage, this, &JsonServer::forwardJsonMessage); - } - } -} - -void JsonServer::forwardJsonMessage(const QJsonObject &message) -{ - QTcpSocket client; - QList list = _hyperion->getForwarder()->getJsonSlaves(); - - for ( int i=0; iwrite(serializedMessage); - if (!socket->waitForBytesWritten()) - { - Debug(_log, "Error while writing data to host"); - return; - } - - // read reply data - QByteArray serializedReply; - while (!serializedReply.contains('\n')) - { - // receive reply - if (!socket->waitForReadyRead()) - { - Debug(_log, "Error while writing data from host"); - return; - } - - serializedReply += socket->readAll(); - } - - // parse reply data - QJsonParseError error; - QJsonDocument reply = QJsonDocument::fromJson(serializedReply ,&error); - - if (error.error != QJsonParseError::NoError) - { - Error(_log, "Error while parsing reply: invalid json"); - return; - } - -} diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index e2169677..885db01f 100755 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -27,6 +27,11 @@ FILE ( GLOB Leddevice_SOURCES "${CURRENT_SOURCE_DIR}/dev_other/*.cpp" ) +if ( ENABLE_OSX ) + 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_USB_HID ) find_package(libusb-1.0 REQUIRED) include_directories( @@ -77,6 +82,7 @@ ENDFOREACH() add_library(leddevice ${CMAKE_BINARY_DIR}/LedDevice_headers.h ${Leddevice_SOURCES} ) target_link_libraries(leddevice + hyperion hyperion-utils ${CMAKE_THREAD_LIBS_INIT} Qt5::Network diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index e12a3277..45331853 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -10,15 +10,10 @@ #include "hyperion/Hyperion.h" #include -LedDeviceRegistry LedDevice::_ledDeviceMap = LedDeviceRegistry(); -QString LedDevice::_activeDevice = ""; -int LedDevice::_ledCount = 0; -int LedDevice::_ledRGBCount = 0; -int LedDevice::_ledRGBWCount= 0; - -LedDevice::LedDevice() - : QObject() - , _log(Logger::getInstance("LedDevice")) +LedDevice::LedDevice(const QJsonObject& config, QObject* parent) + : QObject(parent) + , _devConfig(config) + , _log(Logger::getInstance("LEDDEVICE")) , _ledBuffer(0) , _deviceReady(true) , _refresh_timer() @@ -28,15 +23,16 @@ LedDevice::LedDevice() , _componentRegistered(false) , _enabled(true) { - LedDevice::getLedDeviceSchemas(); - qRegisterMetaType("hyperion::Components"); - // setup timer - _refresh_timer.setSingleShot(false); _refresh_timer.setInterval(0); connect(&_refresh_timer, SIGNAL(timeout()), this, SLOT(rewriteLeds())); } +LedDevice::~LedDevice() +{ + +} + // dummy implemention int LedDevice::open() { @@ -59,17 +55,6 @@ void LedDevice::setEnable(bool enable) _enabled = enable; } -int LedDevice::addToDeviceMap(QString name, LedDeviceCreateFuncType funcPtr) -{ - _ledDeviceMap.emplace(name,funcPtr); - return 0; -} - -const LedDeviceRegistry& LedDevice::getDeviceMap() -{ - return _ledDeviceMap; -} - void LedDevice::setActiveDevice(QString dev) { _activeDevice = dev; @@ -77,6 +62,10 @@ void LedDevice::setActiveDevice(QString dev) bool LedDevice::init(const QJsonObject &deviceConfig) { + _colorOrder = deviceConfig["colorOrder"].toString("RGB"); + _activeDevice = deviceConfig["type"].toString("file").toLower(); + setLedCount(deviceConfig["currentLedCount"].toInt(1)); // property injected to reflect real led count + _latchTime_ms = deviceConfig["latchTime"].toInt(_latchTime_ms); _refresh_timer.setInterval( deviceConfig["rewriteTime"].toInt( _refresh_timer_interval) ); if (_refresh_timer.interval() <= (signed)_latchTime_ms ) @@ -88,42 +77,6 @@ bool LedDevice::init(const QJsonObject &deviceConfig) return true; } -QJsonObject LedDevice::getLedDeviceSchemas() -{ - // make sure the resources are loaded (they may be left out after static linking) - Q_INIT_RESOURCE(LedDeviceSchemas); - - // read the json schema from the resource - QDir d(":/leddevices/"); - QStringList l = d.entryList(); - QJsonObject result, schemaJson; - - for(QString &item : l) - { - QString schemaPath(QString(":/leddevices/")+item); - QString devName = item.remove("schema-"); - - QString data; - if(!FileUtils::readFile(schemaPath, data, Logger::getInstance("LedDevice"))) - { - throw std::runtime_error("ERROR: Schema not found: " + item.toStdString()); - } - - QJsonObject schema; - if(!JsonUtils::parse(schemaPath, data, schema, Logger::getInstance("LedDevice"))) - { - throw std::runtime_error("ERROR: Json schema wrong of file: " + item.toStdString()); - } - - schemaJson = schema; - schemaJson["title"] = QString("edt_dev_spec_header_title"); - - result[devName] = schemaJson; - } - - return result; -} - int LedDevice::setLedValues(const std::vector& ledValues) { int retval = 0; diff --git a/libsrc/leddevice/LedDeviceFactory.cpp b/libsrc/leddevice/LedDeviceFactory.cpp index 85505451..0d38c6ce 100755 --- a/libsrc/leddevice/LedDeviceFactory.cpp +++ b/libsrc/leddevice/LedDeviceFactory.cpp @@ -7,31 +7,22 @@ // Leddevice includes #include +#include #include #include -// following file is auto generated by cmake! it contains all available leddevice headers +// autogen #include "LedDevice_headers.h" -LedDevice * LedDeviceFactory::construct(const QJsonObject & deviceConfig, const int ledCount) +LedDevice * LedDeviceFactory::construct(const QJsonObject & deviceConfig) { - Logger * log = Logger::getInstance("LedDevice"); + Logger * log = Logger::getInstance("LEDDEVICE"); QJsonDocument config(deviceConfig); QString ss(config.toJson(QJsonDocument::Indented)); QString type = deviceConfig["type"].toString("UNSPECIFIED").toLower(); - // set amount of led to leddevice - LedDevice::setLedCount(ledCount); - - #define REGISTER(className) LedDevice::addToDeviceMap(QString(#className).toLower(), LedDevice##className::construct); - - // the REGISTER() calls are autogenerated by cmake. - #include "LedDevice_register.cpp" - - #undef REGISTER - - const LedDeviceRegistry& devList = LedDevice::getDeviceMap(); + const LedDeviceRegistry& devList = LedDeviceWrapper::getDeviceMap(); LedDevice* device = nullptr; try { @@ -40,12 +31,11 @@ LedDevice * LedDeviceFactory::construct(const QJsonObject & deviceConfig, const if (dev.first == type) { device = dev.second(deviceConfig); - LedDevice::setActiveDevice(dev.first); Info(log,"LedDevice '%s' configured.", QSTRING_CSTR(dev.first)); break; } } - + if (device == nullptr) { Error(log, "Dummy device used, because configured device '%s' is unknown", QSTRING_CSTR(type) ); @@ -54,13 +44,11 @@ LedDevice * LedDeviceFactory::construct(const QJsonObject & deviceConfig, const } catch(std::exception& e) { - + Error(log, "Dummy device used, because configured device '%s' throws error '%s'", QSTRING_CSTR(type), e.what()); const QJsonObject dummyDeviceConfig; device = LedDeviceFile::construct(QJsonObject()); } - device->open(); - return device; } diff --git a/libsrc/leddevice/LedDeviceSchemas.qrc b/libsrc/leddevice/LedDeviceSchemas.qrc index bd7afbae..db9100aa 100644 --- a/libsrc/leddevice/LedDeviceSchemas.qrc +++ b/libsrc/leddevice/LedDeviceSchemas.qrc @@ -29,7 +29,9 @@ schemas/schema-udpraw.json schemas/schema-ws2801.json schemas/schema-ws2812spi.json + schemas/schema-apa104.json schemas/schema-ws281x.json schemas/schema-karate.json + schemas/schema-aurora.json diff --git a/libsrc/leddevice/LedDeviceWrapper.cpp b/libsrc/leddevice/LedDeviceWrapper.cpp new file mode 100644 index 00000000..b067c4d7 --- /dev/null +++ b/libsrc/leddevice/LedDeviceWrapper.cpp @@ -0,0 +1,148 @@ +#include + +#include +#include + +// following file is auto generated by cmake! it contains all available leddevice headers +#include "LedDevice_headers.h" + +// util +#include +#include + +// qt +#include +#include + +LedDeviceRegistry LedDeviceWrapper::_ledDeviceMap = LedDeviceRegistry(); + +LedDeviceWrapper::LedDeviceWrapper(Hyperion* hyperion) + : QObject(hyperion) + , _hyperion(hyperion) + , _ledDevice(nullptr) +{ + // prepare the device constrcutor map + #define REGISTER(className) LedDeviceWrapper::addToDeviceMap(QString(#className).toLower(), LedDevice##className::construct); + + // the REGISTER() calls are autogenerated by cmake. + #include "LedDevice_register.cpp" + + #undef REGISTER + + _hyperion->setNewComponentState(hyperion::COMP_LEDDEVICE, true); +} + +LedDeviceWrapper::~LedDeviceWrapper() +{ + stopDeviceThread(); +} + +void LedDeviceWrapper::createLedDevice(const QJsonObject& config) +{ + if(_ledDevice != nullptr) + { + stopDeviceThread(); + } + + // create thread and device + QThread* thread = new QThread(this); + _ledDevice = LedDeviceFactory::construct(config); + _ledDevice->moveToThread(thread); + // setup thread management + connect(thread, &QThread::started, _ledDevice, &LedDevice::start); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, &QThread::finished, _ledDevice, &QObject::deleteLater); + + // further signals + connect(this, &LedDeviceWrapper::write, _ledDevice, &LedDevice::write); + connect(_ledDevice, &LedDevice::enableStateChanged, this, &LedDeviceWrapper::handleInternalEnableState); + + // start the thread + thread->start(); +} + +const QJsonObject LedDeviceWrapper::getLedDeviceSchemas() +{ + // make sure the resources are loaded (they may be left out after static linking) + Q_INIT_RESOURCE(LedDeviceSchemas); + + // read the json schema from the resource + QDir d(":/leddevices/"); + QStringList l = d.entryList(); + QJsonObject result, schemaJson; + + for(QString &item : l) + { + QString schemaPath(QString(":/leddevices/")+item); + QString devName = item.remove("schema-"); + + QString data; + if(!FileUtils::readFile(schemaPath, data, Logger::getInstance("LedDevice"))) + { + throw std::runtime_error("ERROR: Schema not found: " + item.toStdString()); + } + + QJsonObject schema; + if(!JsonUtils::parse(schemaPath, data, schema, Logger::getInstance("LedDevice"))) + { + throw std::runtime_error("ERROR: Json schema wrong of file: " + item.toStdString()); + } + + schemaJson = schema; + schemaJson["title"] = QString("edt_dev_spec_header_title"); + + result[devName] = schemaJson; + } + + return result; +} + +int LedDeviceWrapper::addToDeviceMap(QString name, LedDeviceCreateFuncType funcPtr) +{ + _ledDeviceMap.emplace(name,funcPtr); + return 0; +} + +const LedDeviceRegistry& LedDeviceWrapper::getDeviceMap() +{ + return _ledDeviceMap; +} + +int LedDeviceWrapper::getLatchTime() +{ + return _ledDevice->getLatchTime(); +} + +const QString & LedDeviceWrapper::getActiveDevice() +{ + return _ledDevice->getActiveDevice(); +} + +const QString & LedDeviceWrapper::getColorOrder() +{ + return _ledDevice->getColorOrder(); +} + +void LedDeviceWrapper::handleComponentState(const hyperion::Components component, const bool state) +{ + if(component == hyperion::COMP_LEDDEVICE) + { + _ledDevice->setEnable(state); + _hyperion->setNewComponentState(hyperion::COMP_LEDDEVICE, _ledDevice->componentState()); + _enabled = state; + } +} + +void LedDeviceWrapper::handleInternalEnableState(bool newState) +{ + _hyperion->setNewComponentState(hyperion::COMP_LEDDEVICE, newState); + _enabled = newState; +} + +void LedDeviceWrapper::stopDeviceThread() +{ + _ledDevice->switchOff(); + QThread* oldThread = _ledDevice->thread(); + delete _ledDevice; // fast desctruction + oldThread->quit(); // non blocking +} diff --git a/libsrc/leddevice/dev_net/LedDeviceAurora.cpp b/libsrc/leddevice/dev_net/LedDeviceAurora.cpp new file mode 100644 index 00000000..9ebb91b2 --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceAurora.cpp @@ -0,0 +1,196 @@ + +// Local-Hyperion includes +#include "LedDeviceAurora.h" +#include +#include +// qt includes +#include +#include +#include + +#define ll ss + +struct addrinfo vints, *serverinfo, *pt; +//char udpbuffer[1024]; +int sockfp; +int update_num; +LedDevice* LedDeviceAurora::construct(const QJsonObject &deviceConfig) +{ + return new LedDeviceAurora(deviceConfig); +} + +LedDeviceAurora::LedDeviceAurora(const QJsonObject &deviceConfig) { + init(deviceConfig); +} + +bool LedDeviceAurora::init(const QJsonObject &deviceConfig) { + const QString hostname = deviceConfig["output"].toString(); + const QString key = deviceConfig["key"].toString(); + + manager = new QNetworkAccessManager(); + QString port; + // Read Panel count and panel Ids + QByteArray response = get(hostname, key, "panelLayout/layout"); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(response, &error); + if (error.error != QJsonParseError::NoError) + { + throw std::runtime_error("No Layout found. Check hostname and auth key"); + } + //Debug + QString strJson(doc.toJson(QJsonDocument::Compact)); + std::cout << strJson.toUtf8().constData() << std::endl; + + QJsonObject json = doc.object(); + + panelCount = json["numPanels"].toInt(); + std::cout << panelCount << std::endl; + QJsonObject positionDataJson = doc.object()["positionData"].toObject(); + QJsonArray positionData = json["positionData"].toArray(); + // Loop over all children. + foreach (const QJsonValue & value, positionData) { + QJsonObject panelObj = value.toObject(); + int panelId = panelObj["panelId"].toInt(); + panelIds.push_back(panelId); + } + + // Check if we found enough lights. + if (panelIds.size() != panelCount) { + throw std::runtime_error("Not enough lights found"); + }else { + std::cout << "All panel Ids found: "<< panelIds.size() << std::endl; + } + + // Set Aurora to UDP Mode + QByteArray modeResponse = changeMode(hostname, key, "effects"); + QJsonDocument configDoc = QJsonDocument::fromJson(modeResponse, &error); + + //Debug + //QString strConf(configDoc.toJson(QJsonDocument::Compact)); + //std::cout << strConf.toUtf8().constData() << std::endl; + + if (error.error != QJsonParseError::NoError) + { + throw std::runtime_error("Could not change mode"); + } + + // Get UDP port + port = QString::number(configDoc.object()["streamControlPort"].toInt()); + + std::cout << "hostname " << hostname.toStdString() << " port " << port.toStdString() << std::endl; + + int rv; + + memset(&vints, 0, sizeof vints); + vints.ai_family = AF_UNSPEC; + vints.ai_socktype = SOCK_DGRAM; + + if ((rv = getaddrinfo(hostname.toUtf8().constData() , port.toUtf8().constData(), &vints, &serverinfo)) != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + assert(rv==0); + } + + // loop through all the results and make a socket + for(pt = serverinfo; pt != NULL; pt = pt->ai_next) { + if ((sockfp = socket(pt->ai_family, pt->ai_socktype, + pt->ai_protocol)) == -1) { + perror("talker: socket"); + continue; + } + + break; + } + + if (pt == NULL) { + fprintf(stderr, "talker: failed to create socket\n"); + assert(pt!=NULL); + } + std::cout << "Started successfully "; + return true; +} + +QString LedDeviceAurora::getUrl(QString host, QString token, QString route) { + return QString("http://%1:16021/api/v1/%2/%3").arg(host).arg(token).arg(route); +} + +QByteArray LedDeviceAurora::get(QString host, QString token, QString route) { + QString url = getUrl(host, token, route); + // Perfrom request + QNetworkRequest request(url); + QNetworkReply* reply = manager->get(request); + // Connect requestFinished signal to quit slot of the loop. + QEventLoop loop; + loop.connect(reply, SIGNAL(finished()), SLOT(quit())); + // Go into the loop until the request is finished. + loop.exec(); + // Read all data of the response. + QByteArray response = reply->readAll(); + // Free space. + reply->deleteLater(); + // Return response + return response; +} + +QByteArray LedDeviceAurora::putJson(QString url, QString json) { + // Perfrom request + QNetworkRequest request(url); + QNetworkReply* reply = manager->put(request, json.toUtf8()); + // Connect requestFinished signal to quit slot of the loop. + QEventLoop loop; + loop.connect(reply, SIGNAL(finished()), SLOT(quit())); + // Go into the loop until the request is finished. + loop.exec(); + // Read all data of the response. + QByteArray response = reply->readAll(); + // Free space. + reply->deleteLater(); + // Return response + return response; +} + +QByteArray LedDeviceAurora::changeMode(QString host, QString token, QString route) { + QString url = getUrl(host, token, route); + QString jsondata( "{\"write\" : {\"command\" : \"display\", \"animType\" : \"extControl\"}}"); //Enable UDP Mode + return putJson(url, jsondata); +} + +LedDeviceAurora::~LedDeviceAurora() +{ + delete manager; +} + +int LedDeviceAurora::write(const std::vector & ledValues) +{ + uint udpBufferSize = panelCount * 7 + 1; + char udpbuffer[udpBufferSize]; + update_num++; + update_num &= 0xf; + + int i=0; + int panelCounter = 0; + udpbuffer[i++] = panelCount; + for (const ColorRgb& color : ledValues) + { + if ((unsigned)i < udpBufferSize) { + udpbuffer[i++] = panelIds[panelCounter++ % panelCount]; + udpbuffer[i++] = 1; // No of Frames + udpbuffer[i++] = color.red; + udpbuffer[i++] = color.green; + udpbuffer[i++] = color.blue; + udpbuffer[i++] = 0; // W not set manually + udpbuffer[i++] = 1; // currently fixed at value 1 which corresponds to 100ms + } + if((unsigned)panelCounter > panelCount) { + break; + } + //printf ("c.red %d sz c.red %d\n", color.red, sizeof(color.red)); + } + sendto(sockfp, udpbuffer, i, 0, pt->ai_addr, pt->ai_addrlen); + + return 0; +} + +int LedDeviceAurora::switchOff() +{ + return 0; +} diff --git a/libsrc/leddevice/dev_net/LedDeviceAurora.h b/libsrc/leddevice/dev_net/LedDeviceAurora.h new file mode 100644 index 00000000..dad09a64 --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceAurora.h @@ -0,0 +1,63 @@ +#pragma once + +// Leddevice includes +#include +// Qt includes +#include +#include +#include +#include +/// +/// Implementation of the LedDevice that write the led-colors to an +/// ASCII-textfile('/home/pi/LedDevice.out') +/// +class LedDeviceAurora : public LedDevice +{ +public: + /// + /// Constructs the test-device, which opens an output stream to the file + /// + LedDeviceAurora(const QJsonObject &deviceConfig); + + /// + /// Destructor of this test-device + /// + virtual ~LedDeviceAurora(); + + /// Switch the leds off + virtual int switchOff(); + /// constructs leddevice + static LedDevice* construct(const QJsonObject &deviceConfig); +protected: + /// + /// Writes the RGB-Color values to the leds. + /// + /// @param[in] ledValues The RGB-color per led + /// + /// @return Zero on success else negative + /// + virtual int write(const std::vector & ledValues); + + bool init(const QJsonObject &deviceConfig); + +private: + /// The outputstream + // std::ofstream _ofs; + // QNetworkAccessManager object for sending requests. + QNetworkAccessManager* manager; + + // the number of leds (needed when switching off) + + size_t panelCount; + /// Array of the pannel ids. + std::vector panelIds; + QByteArray get(QString host, QString token, QString route); + QByteArray putJson(QString url, QString json); + QByteArray changeMode(QString host, QString token, QString route); + /// + /// @param route + /// + /// @return the full URL of the request. + /// + QString getUrl(QString host, QString token, QString route); +}; diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index 1830394b..4a66afa9 100755 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -201,7 +201,7 @@ void PhilipsHueBridge::resolveReply(QNetworkReply* reply) void PhilipsHueBridge::post(QString route, QString content) { - Debug(log, "Post %s: %s", QSTRING_CSTR(QString("http://IP/api/USR/%1").arg(route)), QSTRING_CSTR(content)); + //Debug(log, "Post %s: %s", QSTRING_CSTR(QString("http://IP/api/USR/%1").arg(route)), QSTRING_CSTR(content)); QNetworkRequest request(QString("http://%1/api/%2/%3").arg(host).arg(username).arg(route)); manager.put(request, content.toLatin1()); @@ -214,7 +214,7 @@ const std::set PhilipsHueLight::GAMUT_B_MODEL_IDS = const std::set PhilipsHueLight::GAMUT_C_MODEL_IDS = { "LLC020", "LST002", "LCT011", "LCT012", "LCT010", "LCT014" }; -PhilipsHueLight::PhilipsHueLight(Logger* log, PhilipsHueBridge& bridge, unsigned int id, QJsonObject values) +PhilipsHueLight::PhilipsHueLight(Logger* log, PhilipsHueBridge* bridge, unsigned int id, QJsonObject values) : log(log) , bridge(bridge) , id(id) @@ -297,7 +297,7 @@ PhilipsHueLight::~PhilipsHueLight() void PhilipsHueLight::set(QString state) { - bridge.post(QString("lights/%1/state").arg(id), state); + bridge->post(QString("lights/%1/state").arg(id), state); } void PhilipsHueLight::setOn(bool on) @@ -345,18 +345,25 @@ LedDevice* LedDevicePhilipsHue::construct(const QJsonObject &deviceConfig) } LedDevicePhilipsHue::LedDevicePhilipsHue(const QJsonObject& deviceConfig) - : LedDevice() - , bridge(_log, deviceConfig["output"].toString(), deviceConfig["username"].toString()) + : LedDevice(deviceConfig) + , _bridge(nullptr) { - _deviceReady = init(deviceConfig); - connect(&bridge, &PhilipsHueBridge::newLights, this, &LedDevicePhilipsHue::newLights); - connect(this, &LedDevice::enableStateChanged, this, &LedDevicePhilipsHue::stateChanged); } LedDevicePhilipsHue::~LedDevicePhilipsHue() { switchOff(); + delete _bridge; +} + +void LedDevicePhilipsHue::start() +{ + _bridge = new PhilipsHueBridge(_log, _devConfig["output"].toString(), _devConfig["username"].toString()); + _deviceReady = init(_devConfig); + + connect(_bridge, &PhilipsHueBridge::newLights, this, &LedDevicePhilipsHue::newLights); + connect(this, &LedDevice::enableStateChanged, this, &LedDevicePhilipsHue::stateChanged); } bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) @@ -374,7 +381,7 @@ bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) lightIds.push_back(i.toInt()); } // get light info from bridge - bridge.bConnect(); + _bridge->bConnect(); // adapt latchTime to count of user lightIds (bridge 10Hz max overall) newDC.insert("latchTime",QJsonValue(100*(int)lightIds.size())); @@ -399,7 +406,7 @@ void LedDevicePhilipsHue::newLights(QMap map) { if (map.contains(id)) { - lights.push_back(PhilipsHueLight(_log, bridge, id, map.value(id))); + lights.push_back(PhilipsHueLight(_log, _bridge, id, map.value(id))); } else { @@ -427,7 +434,6 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) { // Get color. ColorRgb color = ledValues.at(idx); - // Scale colors from [0, 255] to [0, 1] and convert to xy space. CiColor xy = CiColor::rgbToCiColor(color.red / 255.0f, color.green / 255.0f, color.blue / 255.0f, light.getColorSpace()); @@ -453,7 +459,7 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) void LedDevicePhilipsHue::stateChanged(bool newState) { if(newState) - bridge.bConnect(); + _bridge->bConnect(); else lights.clear(); } diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h index cc959ab1..0403b388 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.h @@ -139,7 +139,7 @@ class PhilipsHueLight { private: Logger* log; - PhilipsHueBridge& bridge; + PhilipsHueBridge* bridge; /// light id unsigned int id; bool on; @@ -172,7 +172,7 @@ public: /// @param bridge the bridge /// @param id the light id /// - PhilipsHueLight(Logger* log, PhilipsHueBridge& bridge, unsigned int id, QJsonObject values); + PhilipsHueLight(Logger* log, PhilipsHueBridge* bridge, unsigned int id, QJsonObject values); ~PhilipsHueLight(); /// @@ -227,6 +227,10 @@ public: /// constructs leddevice static LedDevice* construct(const QJsonObject &deviceConfig); +public slots: + /// thread start + virtual void start(); + private slots: /// creates new PhilipsHueLight(s) based on user lightid with bridge feedback /// @@ -249,7 +253,7 @@ protected: private: /// bridge class - PhilipsHueBridge bridge; + PhilipsHueBridge* _bridge; /// bool switchOffOnBlack; diff --git a/libsrc/leddevice/dev_net/ProviderUdp.cpp b/libsrc/leddevice/dev_net/ProviderUdp.cpp index b49d098a..d40ffa48 100644 --- a/libsrc/leddevice/dev_net/ProviderUdp.cpp +++ b/libsrc/leddevice/dev_net/ProviderUdp.cpp @@ -21,7 +21,7 @@ ProviderUdp::ProviderUdp() , _defaultHost("127.0.0.1") { _latchTime_ms = 1; - _udpSocket = new QUdpSocket(); + _udpSocket = new QUdpSocket(this); } ProviderUdp::~ProviderUdp() @@ -34,7 +34,7 @@ bool ProviderUdp::init(const QJsonObject &deviceConfig) LedDevice::init(deviceConfig); QString host = deviceConfig["host"].toString(_defaultHost); - + if (_address.setAddress(host) ) { Debug( _log, "Successfully parsed %s as an ip address.", deviceConfig["host"].toString().toStdString().c_str()); @@ -57,9 +57,9 @@ bool ProviderUdp::init(const QJsonObject &deviceConfig) { throw std::runtime_error("invalid target port"); } - + Debug( _log, "UDP using %s:%d", _address.toString().toStdString().c_str() , _port ); - + return true; } diff --git a/libsrc/leddevice/dev_net/ProviderUdp.h b/libsrc/leddevice/dev_net/ProviderUdp.h index ef34220d..316902bd 100644 --- a/libsrc/leddevice/dev_net/ProviderUdp.h +++ b/libsrc/leddevice/dev_net/ProviderUdp.h @@ -1,11 +1,14 @@ #pragma once -#include - // Hyperion includes #include #include +// qt +#include + +class QUdpSocket; + /// /// The ProviderUdp implements an abstract base-class for LedDevices using UDP packets. /// diff --git a/libsrc/leddevice/dev_spi/LedDeviceAPA104.cpp b/libsrc/leddevice/dev_spi/LedDeviceAPA104.cpp new file mode 100644 index 00000000..f134cb03 --- /dev/null +++ b/libsrc/leddevice/dev_spi/LedDeviceAPA104.cpp @@ -0,0 +1,95 @@ +#include "LedDeviceAPA104.h" + +/* +From the data sheet: + +(TH+TL=1.25μs±600ns) + +T0H, 0 code, high level time, 350ns ±150ns +T0L, 0 code, low level time, 1360ns ±150ns +T1H, 1 code, high level time, 1360ns ±150ns +T1L, 1 code, low level time, 350ns ±150ns +WT, Wait for the processing time, NA +Trst, Reset code,low level time, 24µs + +To normalise the pulse times so they fit in 4 SPI bits: + +On the assumption that the "low" time doesnt matter much + +A SPI bit time of 0.40uS = 2.5 Mbit/sec +T0 is sent as 1000 +T1 is sent as 1110 + +With a bit of excel testing, we can work out the maximum and minimum speeds: +2000000 MIN +2235000 AVG +2470000 MAX + +Wait time: +Not Applicable for WS2812 + +Reset time: +using the max of 2470000, the bit time is 405nS +Reset time is 24uS = 59 bits = 8 bytes + +*/ + +LedDeviceAPA104::LedDeviceAPA104(const QJsonObject &deviceConfig) + : ProviderSpi() + , SPI_BYTES_PER_COLOUR(4) + , SPI_FRAME_END_LATCH_BYTES(8) + , bitpair_to_byte { + 0b10001000, + 0b10001110, + 0b11101000, + 0b11101110, + } +{ + _deviceReady = init(deviceConfig); +} + +LedDevice* LedDeviceAPA104::construct(const QJsonObject &deviceConfig) +{ + return new LedDeviceAPA104(deviceConfig); +} + +bool LedDeviceAPA104::init(const QJsonObject &deviceConfig) +{ + _baudRate_Hz = 2235000; + if ( !ProviderSpi::init(deviceConfig) ) + { + return false; + } + WarningIf(( _baudRate_Hz < 2000000 || _baudRate_Hz > 2470000 ), _log, "SPI rate %d outside recommended range (2000000 -> 2470000)", _baudRate_Hz); + + _ledBuffer.resize(_ledRGBCount * SPI_BYTES_PER_COLOUR + SPI_FRAME_END_LATCH_BYTES, 0x00); + + return true; +} + +int LedDeviceAPA104::write(const std::vector &ledValues) +{ + unsigned spi_ptr = 0; + const int SPI_BYTES_PER_LED = sizeof(ColorRgb) * SPI_BYTES_PER_COLOUR; + + for (const ColorRgb& color : ledValues) + { + uint32_t colorBits = ((unsigned int)color.red << 16) + | ((unsigned int)color.green << 8) + | color.blue; + + for (int j=SPI_BYTES_PER_LED - 1; j>=0; j--) + { + _ledBuffer[spi_ptr+j] = bitpair_to_byte[ colorBits & 0x3 ]; + colorBits >>= 2; + } + spi_ptr += SPI_BYTES_PER_LED; + } + + for (int j=0; j < SPI_FRAME_END_LATCH_BYTES; j++) + { + _ledBuffer[spi_ptr++] = 0; + } + + return writeBytes(_ledBuffer.size(), _ledBuffer.data()); +} diff --git a/libsrc/leddevice/dev_spi/LedDeviceAPA104.h b/libsrc/leddevice/dev_spi/LedDeviceAPA104.h new file mode 100644 index 00000000..23f9500d --- /dev/null +++ b/libsrc/leddevice/dev_spi/LedDeviceAPA104.h @@ -0,0 +1,43 @@ +#pragma once + +// hyperion incluse +#include "ProviderSpi.h" + +/// +/// Implementation of the LedDevice interface for writing to APA104 led device via spi. +/// +class LedDeviceAPA104 : public ProviderSpi +{ +public: + /// + /// Constructs specific LedDevice + /// + /// @param deviceConfig json device config + /// + LedDeviceAPA104(const QJsonObject &deviceConfig); + + /// constructs leddevice + static LedDevice* construct(const QJsonObject &deviceConfig); + + /// + /// Sets configuration + /// + /// @param deviceConfig the json device config + /// @return true if success + virtual bool init(const QJsonObject &deviceConfig); + +private: + /// + /// Writes the led color values to the led-device + /// + /// @param ledValues The color-value per led + /// @return Zero on succes else negative + /// + virtual int write(const std::vector &ledValues); + + const int SPI_BYTES_PER_COLOUR; + + const int SPI_FRAME_END_LATCH_BYTES; + + uint8_t bitpair_to_byte[4]; +}; diff --git a/libsrc/leddevice/dev_tinker/LedDeviceTinkerforge.cpp b/libsrc/leddevice/dev_tinker/LedDeviceTinkerforge.cpp index b0c319ae..d1eac74d 100644 --- a/libsrc/leddevice/dev_tinker/LedDeviceTinkerforge.cpp +++ b/libsrc/leddevice/dev_tinker/LedDeviceTinkerforge.cpp @@ -40,7 +40,7 @@ bool LedDeviceTinkerforge::init(const QJsonObject &deviceConfig) _uid = deviceConfig["uid"].toString(); _interval = deviceConfig["rate"].toInt(); - if ((unsigned)_ledCount > MAX_NUM_LEDS) + if ((unsigned)_ledCount > MAX_NUM_LEDS) { Error(_log,"Invalid attempt to write led values. Not more than %d leds are allowed.", MAX_NUM_LEDS); return -1; @@ -68,7 +68,7 @@ int LedDeviceTinkerforge::open() if (_ipConnection != nullptr) { Error(_log, "Attempt to open existing connection; close before opening"); - return -1; + return 0; } // Initialise a new connection @@ -76,10 +76,10 @@ int LedDeviceTinkerforge::open() ipcon_create(_ipConnection); int connectionStatus = ipcon_connect(_ipConnection, QSTRING_CSTR(_host), _port); - if (connectionStatus < 0) + if (connectionStatus < 0) { - Warning(_log, "Attempt to connect to master brick (%s:%d) failed with status %d", QSTRING_CSTR(_host), _port, connectionStatus); - return -1; + Error(_log, "Attempt to connect to master brick (%s:%d) failed with status %d", QSTRING_CSTR(_host), _port, connectionStatus); + return 0; } // Create the 'LedStrip' @@ -87,17 +87,20 @@ int LedDeviceTinkerforge::open() led_strip_create(_ledStrip, QSTRING_CSTR(_uid), _ipConnection); int frameStatus = led_strip_set_frame_duration(_ledStrip, _interval); - if (frameStatus < 0) + if (frameStatus < 0) { Error(_log,"Attempt to connect to led strip bricklet (led_strip_set_frame_duration()) failed with status %d", frameStatus); - return -1; + return 0; } - return 0; + return 1; } int LedDeviceTinkerforge::write(const std::vector &ledValues) { + if(!_deviceReady) + return 0; + auto redIt = _redChannel.begin(); auto greenIt = _greenChannel.begin(); auto blueIt = _blueChannel.begin(); @@ -115,7 +118,7 @@ int LedDeviceTinkerforge::write(const std::vector &ledValues) return transferLedData(_ledStrip, 0, _colorChannelSize, _redChannel.data(), _greenChannel.data(), _blueChannel.data()); } -int LedDeviceTinkerforge::transferLedData(LEDStrip *ledStrip, unsigned index, unsigned length, uint8_t *redChannel, uint8_t *greenChannel, uint8_t *blueChannel) +int LedDeviceTinkerforge::transferLedData(LEDStrip *ledStrip, unsigned index, unsigned length, uint8_t *redChannel, uint8_t *greenChannel, uint8_t *blueChannel) { if (length == 0 || index >= length || length > MAX_NUM_LEDS) { diff --git a/libsrc/leddevice/schemas/schema-apa104.json b/libsrc/leddevice/schemas/schema-apa104.json new file mode 100644 index 00000000..0478669d --- /dev/null +++ b/libsrc/leddevice/schemas/schema-apa104.json @@ -0,0 +1,35 @@ +{ + "type":"object", + "required":true, + "properties":{ + "output": { + "type": "string", + "title":"edt_dev_spec_spipath_title", + "enum" : ["/dev/spidev0.0","/dev/spidev0.1"], + "propertyOrder" : 1 + }, + "rate": { + "type": "integer", + "title":"edt_dev_spec_baudrate_title", + "default": 2235000, + "propertyOrder" : 2 + }, + "invert": { + "type": "boolean", + "title":"edt_dev_spec_invert_title", + "default": false, + "propertyOrder" : 3 + }, + "latchTime": { + "type": "integer", + "title":"edt_dev_spec_latchtime_title", + "default": 1, + "append" : "edt_append_ms", + "minimum": 1, + "maximum": 1000, + "access" : "expert", + "propertyOrder" : 4 + } + }, + "additionalProperties": true +} diff --git a/libsrc/leddevice/schemas/schema-aurora.json b/libsrc/leddevice/schemas/schema-aurora.json new file mode 100644 index 00000000..a116149f --- /dev/null +++ b/libsrc/leddevice/schemas/schema-aurora.json @@ -0,0 +1,17 @@ +{ + "type":"object", + "required":true, + "properties":{ + "output": { + "type": "string", + "title":"edt_dev_spec_targetIp_title", + "propertyOrder" : 1 + }, + "key": { + "type": "string", + "title":"edt_dev_auth_key_title", + "propertyOrder" : 2 + } + }, + "additionalProperties": true +} diff --git a/libsrc/protoserver/CMakeLists.txt b/libsrc/protoserver/CMakeLists.txt deleted file mode 100644 index 737292f2..00000000 --- a/libsrc/protoserver/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ - -# 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} -) -FILE ( GLOB ProtoServer_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -set(ProtoServer_PROTOS ${CURRENT_SOURCE_DIR}/message.proto ) - -protobuf_generate_cpp(ProtoServer_PROTO_SRCS ProtoServer_PROTO_HDRS ${ProtoServer_PROTOS} ) - -add_library(protoserver - ${ProtoServer_SOURCES} - ${ProtoServer_PROTOS} - ${ProtoServer_PROTO_SRCS} - ${ProtoServer_PROTO_HDRS} -) -# disable warnings for auto generatet proto files, we can't change the files .... -SET_SOURCE_FILES_PROPERTIES ( ${ProtoServer_PROTO_SRCS} ${ProtoServer_PROTO_HDRS} ${ProtoServer_PROTOS} PROPERTIES COMPILE_FLAGS -w ) - -target_link_libraries(protoserver - hyperion - hyperion-utils - protobuf - Qt5::Gui -) diff --git a/libsrc/protoserver/ProtoClientConnection.cpp b/libsrc/protoserver/ProtoClientConnection.cpp deleted file mode 100644 index 1e2e24e2..00000000 --- a/libsrc/protoserver/ProtoClientConnection.cpp +++ /dev/null @@ -1,255 +0,0 @@ -// system includes -#include -#include - -// stl includes -#include -#include -#include - -// Qt includes -#include -#include -#include -#include - -// hyperion util includes -#include "hyperion/ImageProcessorFactory.h" -#include "hyperion/ImageProcessor.h" -#include "utils/ColorRgb.h" - -// project includes -#include "ProtoClientConnection.h" - -ProtoClientConnection::ProtoClientConnection(QTcpSocket *socket) - : QObject() - , _socket(socket) - , _imageProcessor(ImageProcessorFactory::getInstance().newImageProcessor()) - , _hyperion(Hyperion::getInstance()) - , _receiveBuffer() - , _priority(-1) - , _priorityChannelName("Proto-Server") - , _clientAddress(QHostInfo::fromName(socket->peerAddress().toString()).hostName()) -{ - // connect internal signals and slots - connect(_socket, SIGNAL(disconnected()), this, SLOT(socketClosed())); - connect(_socket, SIGNAL(readyRead()), this, SLOT(readData())); - connect(_hyperion, SIGNAL(imageToLedsMappingChanged(int)), _imageProcessor, SLOT(setLedMappingType(int))); -} - -ProtoClientConnection::~ProtoClientConnection() -{ - delete _socket; -} - -void ProtoClientConnection::readData() -{ - _receiveBuffer += _socket->readAll(); - - // check if we can read a message size - if (_receiveBuffer.size() <= 4) - { - return; - } - - // read the message size - uint32_t messageSize = - ((_receiveBuffer[0]<<24) & 0xFF000000) | - ((_receiveBuffer[1]<<16) & 0x00FF0000) | - ((_receiveBuffer[2]<< 8) & 0x0000FF00) | - ((_receiveBuffer[3] ) & 0x000000FF); - - // check if we can read a complete message - if ((uint32_t) _receiveBuffer.size() < messageSize + 4) - { - return; - } - - // read a message - proto::HyperionRequest message; - if (!message.ParseFromArray(_receiveBuffer.data() + 4, messageSize)) - { - sendErrorReply("Unable to parse message"); - } - - // handle the message - handleMessage(message); - - // remove message data from buffer - _receiveBuffer = _receiveBuffer.mid(messageSize + 4); -} - -void ProtoClientConnection::socketClosed() -{ - _hyperion->unRegisterPriority(_priorityChannelName); - emit connectionClosed(this); -} - -void ProtoClientConnection::setVideoMode(const VideoMode videoMode) -{ - int video_Mode = (int)videoMode; - proto::HyperionReply vMode; - - // create proto message - vMode.set_type(proto::HyperionReply::VIDEO); - vMode.set_video(video_Mode); - - // send message - sendMessage(vMode); -} - -void ProtoClientConnection::handleMessage(const proto::HyperionRequest & message) -{ - // forward messages - emit newMessage(&message); - - int prevPriority = _priority; - switch (message.command()) - { - case proto::HyperionRequest::COLOR: - if (!message.HasExtension(proto::ColorRequest::colorRequest)) - { - sendErrorReply("Received COLOR command without ColorRequest"); - break; - } - handleColorCommand(message.GetExtension(proto::ColorRequest::colorRequest)); - break; - case proto::HyperionRequest::IMAGE: - if (!message.HasExtension(proto::ImageRequest::imageRequest)) - { - sendErrorReply("Received IMAGE command without ImageRequest"); - break; - } - handleImageCommand(message.GetExtension(proto::ImageRequest::imageRequest)); - break; - case proto::HyperionRequest::CLEAR: - if (!message.HasExtension(proto::ClearRequest::clearRequest)) - { - sendErrorReply("Received CLEAR command without ClearRequest"); - break; - } - handleClearCommand(message.GetExtension(proto::ClearRequest::clearRequest)); - break; - case proto::HyperionRequest::CLEARALL: - handleClearallCommand(); - break; - default: - handleNotImplemented(); - } - - if (prevPriority != _priority) - { - _hyperion->registerPriority(_priorityChannelName, _priority); - prevPriority = _priority; - } -} - -void ProtoClientConnection::handleColorCommand(const proto::ColorRequest &message) -{ - // extract parameters - _priority = message.priority(); - int duration = message.has_duration() ? message.duration() : -1; - ColorRgb color; - color.red = qRed(message.rgbcolor()); - color.green = qGreen(message.rgbcolor()); - color.blue = qBlue(message.rgbcolor()); - - // set output - _hyperion->setColor(_priority, color, duration); - - // send reply - sendSuccessReply(); -} - -void ProtoClientConnection::handleImageCommand(const proto::ImageRequest &message) -{ - // extract parameters - _priority = message.priority(); - int duration = message.has_duration() ? message.duration() : -1; - int width = message.imagewidth(); - int height = message.imageheight(); - const std::string & imageData = message.imagedata(); - - // check consistency of the size of the received data - if ((int) imageData.size() != width*height*3) - { - sendErrorReply("Size of image data does not match with the width and height"); - return; - } - - // set width and height of the image processor - _imageProcessor->setSize(width, height); - - // create ImageRgb - Image image(width, height); - memcpy(image.memptr(), imageData.c_str(), imageData.size()); - - // process the image - std::vector ledColors = _imageProcessor->process(image); - _hyperion->setColors(_priority, ledColors, duration, true, hyperion::COMP_PROTOSERVER , "proto@"+_clientAddress); - _hyperion->setImage(_priority, image, duration); - - // send reply - sendSuccessReply(); -} - - -void ProtoClientConnection::handleClearCommand(const proto::ClearRequest &message) -{ - // extract parameters - _priority = message.priority(); - - // clear priority - _hyperion->clear(_priority); - _hyperion->unRegisterPriority(_priorityChannelName); - // send reply - sendSuccessReply(); -} - -void ProtoClientConnection::handleClearallCommand() -{ - // clear priority - _hyperion->clearall(); - - // send reply - sendSuccessReply(); -} - - -void ProtoClientConnection::handleNotImplemented() -{ - sendErrorReply("Command not implemented"); -} - -void ProtoClientConnection::sendMessage(const google::protobuf::Message &message) -{ - std::string serializedReply = message.SerializeAsString(); - uint32_t size = serializedReply.size(); - uint8_t sizeData[] = {uint8_t(size >> 24), uint8_t(size >> 16), uint8_t(size >> 8), uint8_t(size)}; - _socket->write((const char *) sizeData, sizeof(sizeData)); - _socket->write(serializedReply.data(), serializedReply.length()); - _socket->flush(); -} - -void ProtoClientConnection::sendSuccessReply() -{ - // create reply - proto::HyperionReply reply; - reply.set_type(proto::HyperionReply::REPLY); - reply.set_success(true); - - // send reply - sendMessage(reply); -} - -void ProtoClientConnection::sendErrorReply(const std::string &error) -{ - // create reply - proto::HyperionReply reply; - reply.set_type(proto::HyperionReply::REPLY); - reply.set_success(false); - reply.set_error(error); - - // send reply - sendMessage(reply); -} diff --git a/libsrc/protoserver/ProtoClientConnection.h b/libsrc/protoserver/ProtoClientConnection.h deleted file mode 100644 index ad139094..00000000 --- a/libsrc/protoserver/ProtoClientConnection.h +++ /dev/null @@ -1,146 +0,0 @@ -#pragma once - -// stl includes -#include - -// Qt includes -#include -#include -#include -#include - -// Hyperion includes -#include - -//Utils includes -#include - -// proto includes -#include "message.pb.h" -#include "protoserver/ProtoConnection.h" - -class ImageProcessor; - -/// -/// The Connection object created by a ProtoServer when a new connection is establshed -/// -class ProtoClientConnection : public QObject -{ - Q_OBJECT - -public: - /// - /// Constructor - /// @param socket The Socket object for this connection - /// @param hyperion The Hyperion server - /// - ProtoClientConnection(QTcpSocket * socket); - - /// - /// Destructor - /// - ~ProtoClientConnection(); - -public slots: - /// - /// Send video mode message to connected client - /// - void setVideoMode(const VideoMode videoMode); - -signals: - /// - /// Signal which is emitted when the connection is being closed - /// @param connection This connection object - /// - void connectionClosed(ProtoClientConnection * connection); - void newMessage(const proto::HyperionRequest * message); - -private slots: - /// - /// Slot called when new data has arrived - /// - void readData(); - - /// - /// Slot called when this connection is being closed - /// - void socketClosed(); - -private: - /// - /// Handle an incoming Proto message - /// - /// @param message the incoming message as string - /// - void handleMessage(const proto::HyperionRequest &message); - - /// - /// Handle an incoming Proto Color message - /// - /// @param message the incoming message - /// - void handleColorCommand(const proto::ColorRequest & message); - - /// - /// Handle an incoming Proto Image message - /// - /// @param message the incoming message - /// - void handleImageCommand(const proto::ImageRequest & message); - - /// - /// Handle an incoming Proto Clear message - /// - /// @param message the incoming message - /// - void handleClearCommand(const proto::ClearRequest & message); - - /// - /// Handle an incoming Proto Clearall message - /// - void handleClearallCommand(); - - /// - /// Handle an incoming Proto message of unknown type - /// - void handleNotImplemented(); - - /// - /// Send a message to the connected client - /// - /// @param message The Proto message to send - /// - void sendMessage(const google::protobuf::Message &message); - - /// - /// Send a standard reply indicating success - /// - void sendSuccessReply(); - - /// - /// Send an error message back to the client - /// - /// @param error String describing the error - /// - void sendErrorReply(const std::string & error); - -private: - /// The TCP-Socket that is connected tot the Proto-client - QTcpSocket * _socket; - - /// The processor for translating images to led-values - ImageProcessor * _imageProcessor; - - /// Link to Hyperion for writing led-values to a priority channel - Hyperion * _hyperion; - - /// The buffer used for reading data from the socket - QByteArray _receiveBuffer; - - int _priority; - - QString _priorityChannelName; - - /// address of client - QString _clientAddress; -}; diff --git a/libsrc/protoserver/ProtoConnection.cpp b/libsrc/protoserver/ProtoConnection.cpp deleted file mode 100644 index d723b0ae..00000000 --- a/libsrc/protoserver/ProtoConnection.cpp +++ /dev/null @@ -1,239 +0,0 @@ -// stl includes -#include - -// Qt includes -#include - -// protoserver includes -#include "protoserver/ProtoConnection.h" - -ProtoConnection::ProtoConnection(const QString & address) : - _socket(), - _skipReply(false), - _prevSocketState(QAbstractSocket::UnconnectedState), - _log(Logger::getInstance("PROTOCONNECTION")) - { - QStringList parts = address.split(":"); - if (parts.size() != 2) - { - throw std::runtime_error(QString("PROTOCONNECTION ERROR: Wrong address: Unable to parse address (%1)").arg(address).toStdString()); - } - _host = parts[0]; - - bool ok; - _port = parts[1].toUShort(&ok); - if (!ok) - { - throw std::runtime_error(QString("PROTOCONNECTION ERROR: Wrong port: Unable to parse the port number (%1)").arg(parts[1]).toStdString()); - } - - // try to connect to host - Info(_log, "Connecting to Hyperion: %s:%d", _host.toStdString().c_str(), _port); - connectToHost(); - - // start the connection timer - _timer.setInterval(5000); - _timer.setSingleShot(false); - - connect(&_timer,SIGNAL(timeout()), this, SLOT(connectToHost())); - connect(&_socket, SIGNAL(readyRead()), this, SLOT(readData())); - _timer.start(); -} - -ProtoConnection::~ProtoConnection() -{ - _timer.stop(); - _socket.close(); -} - -void ProtoConnection::readData() -{ - _receiveBuffer += _socket.readAll(); - - // check if we can read a message size - if (_receiveBuffer.size() <= 4) - { - return; - } - - // read the message size - uint32_t messageSize = - ((_receiveBuffer[0]<<24) & 0xFF000000) | - ((_receiveBuffer[1]<<16) & 0x00FF0000) | - ((_receiveBuffer[2]<< 8) & 0x0000FF00) | - ((_receiveBuffer[3] ) & 0x000000FF); - - // check if we can read a complete message - if ((uint32_t) _receiveBuffer.size() < messageSize + 4) - { - return; - } - - // read a message - proto::HyperionReply reply; - - if (!reply.ParseFromArray(_receiveBuffer.data() + 4, messageSize)) - { - Error(_log, "Unable to parse message"); - return; - } - - parseReply(reply); - - // remove message data from buffer - _receiveBuffer = _receiveBuffer.mid(messageSize + 4); -} - -void ProtoConnection::setSkipReply(bool skip) -{ - _skipReply = skip; -} - -void ProtoConnection::setColor(const ColorRgb & color, int priority, int duration) -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::COLOR); - proto::ColorRequest * colorRequest = request.MutableExtension(proto::ColorRequest::colorRequest); - colorRequest->set_rgbcolor((color.red << 16) | (color.green << 8) | color.blue); - colorRequest->set_priority(priority); - colorRequest->set_duration(duration); - - // send command message - sendMessage(request); -} - -void ProtoConnection::setImage(const Image &image, int priority, int duration) -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::IMAGE); - proto::ImageRequest * imageRequest = request.MutableExtension(proto::ImageRequest::imageRequest); - imageRequest->set_imagedata(image.memptr(), image.width() * image.height() * 3); - imageRequest->set_imagewidth(image.width()); - imageRequest->set_imageheight(image.height()); - imageRequest->set_priority(priority); - imageRequest->set_duration(duration); - - // send command message - sendMessage(request); -} - -void ProtoConnection::clear(int priority) -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::CLEAR); - proto::ClearRequest * clearRequest = request.MutableExtension(proto::ClearRequest::clearRequest); - clearRequest->set_priority(priority); - - // send command message - sendMessage(request); -} - -void ProtoConnection::clearAll() -{ - proto::HyperionRequest request; - request.set_command(proto::HyperionRequest::CLEARALL); - - // send command message - sendMessage(request); -} - -void ProtoConnection::connectToHost() -{ - // try connection only when - if (_socket.state() == QAbstractSocket::UnconnectedState) - { - _socket.connectToHost(_host, _port); - //_socket.waitForConnected(1000); - } -} - -void ProtoConnection::sendMessage(const proto::HyperionRequest &message) -{ - // print out connection message only when state is changed - if (_socket.state() != _prevSocketState ) - { - switch (_socket.state() ) - { - case QAbstractSocket::UnconnectedState: - Info(_log, "No connection to Hyperion: %s:%d", _host.toStdString().c_str(), _port); - break; - - case QAbstractSocket::ConnectedState: - Info(_log, "Connected to Hyperion: %s:%d", _host.toStdString().c_str(), _port); - break; - - default: - Debug(_log, "Connecting to Hyperion: %s:%d", _host.toStdString().c_str(), _port); - break; - } - _prevSocketState = _socket.state(); - } - - - if (_socket.state() != QAbstractSocket::ConnectedState) - { - return; - } - - // We only get here if we are connected - - // serialize message (FastWriter already appends a newline) - std::string serializedMessage = message.SerializeAsString(); - - int length = serializedMessage.size(); - const uint8_t header[] = { - uint8_t((length >> 24) & 0xFF), - uint8_t((length >> 16) & 0xFF), - uint8_t((length >> 8) & 0xFF), - uint8_t((length ) & 0xFF)}; - - // write message - int count = 0; - count += _socket.write(reinterpret_cast(header), 4); - count += _socket.write(reinterpret_cast(serializedMessage.data()), length); - if (!_socket.waitForBytesWritten()) - { - Error(_log, "Error while writing data to host"); - return; - } -} - -bool ProtoConnection::parseReply(const proto::HyperionReply &reply) -{ - bool success = false; - - switch (reply.type()) - { - case proto::HyperionReply::REPLY: - { - if (!_skipReply) - { - if (!reply.success()) - { - if (reply.has_error()) - { - throw std::runtime_error("PROTOCONNECTION ERROR: " + reply.error()); - } - else - { - throw std::runtime_error("PROTOCONNECTION ERROR: No error info"); - } - } - else - { - success = true; - } - } - break; - } - case proto::HyperionReply::VIDEO: - { - int video = reply.has_video() ? reply.video() : 0; - VideoMode vMode = (VideoMode)video; - emit setVideoMode(vMode); - break; - } - } - - return success; -} diff --git a/libsrc/protoserver/ProtoConnectionWrapper.cpp b/libsrc/protoserver/ProtoConnectionWrapper.cpp deleted file mode 100644 index 160033e2..00000000 --- a/libsrc/protoserver/ProtoConnectionWrapper.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// protoserver includes -#include "protoserver/ProtoConnectionWrapper.h" - -ProtoConnectionWrapper::ProtoConnectionWrapper(const QString &address, - int priority, - int duration_ms, - bool skipProtoReply) - : _priority(priority) - , _duration_ms(duration_ms) - , _connection(address) -{ - _connection.setSkipReply(skipProtoReply); - connect(&_connection, SIGNAL(setVideoMode(VideoMode)), this, SIGNAL(setVideoMode(VideoMode))); -} - -ProtoConnectionWrapper::~ProtoConnectionWrapper() -{ -} - -void ProtoConnectionWrapper::receiveImage(const Image & image) -{ - _connection.setImage(image, _priority, _duration_ms); -} diff --git a/libsrc/protoserver/ProtoServer.cpp b/libsrc/protoserver/ProtoServer.cpp deleted file mode 100644 index a925bf74..00000000 --- a/libsrc/protoserver/ProtoServer.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// system includes -#include - -// project includes -#include -#include -#include "protoserver/ProtoConnection.h" -#include "ProtoClientConnection.h" - -ProtoServer::ProtoServer(uint16_t port) - : QObject() - , _hyperion(Hyperion::getInstance()) - , _server() - , _openConnections() - , _log(Logger::getInstance("PROTOSERVER")) - , _forwarder_enabled(true) -{ - - MessageForwarder * forwarder = _hyperion->getForwarder(); - QStringList slaves = forwarder->getProtoSlaves(); - - for (int i = 0; i < slaves.size(); ++i) { - if ( QString("127.0.0.1:%1").arg(port) == slaves.at(i) ) { - throw std::runtime_error("PROTOSERVER ERROR: Loop between proto server and forwarder detected. Fix your config!"); - } - - ProtoConnection* p = new ProtoConnection(slaves.at(i).toLocal8Bit().constData()); - p->setSkipReply(true); - _proxy_connections << p; - } - - if (!_server.listen(QHostAddress::Any, port)) - { - throw std::runtime_error("PROTOSERVER ERROR: Could not bind to port"); - } - - // Set trigger for incoming connections - connect(&_server, SIGNAL(newConnection()), this, SLOT(newConnection())); - connect( _hyperion, SIGNAL(componentStateChanged(hyperion::Components,bool)), this, SLOT(componentStateChanged(hyperion::Components,bool))); - -} - -ProtoServer::~ProtoServer() -{ - foreach (ProtoClientConnection * connection, _openConnections) { - delete connection; - } - - while (!_proxy_connections.isEmpty()) - delete _proxy_connections.takeFirst(); -} - -uint16_t ProtoServer::getPort() const -{ - return _server.serverPort(); -} - -void ProtoServer::newConnection() -{ - QTcpSocket * socket = _server.nextPendingConnection(); - - if (socket != nullptr) - { - Debug(_log, "New connection"); - ProtoClientConnection * connection = new ProtoClientConnection(socket); - _openConnections.insert(connection); - - // register slot for cleaning up after the connection closed - connect(connection, SIGNAL(connectionClosed(ProtoClientConnection*)), this, SLOT(closedConnection(ProtoClientConnection*))); - connect(connection, SIGNAL(newMessage(const proto::HyperionRequest*)), this, SLOT(newMessage(const proto::HyperionRequest*))); - - // register forward signal for video mode - connect(this, SIGNAL(videoMode(VideoMode)), connection, SLOT(setVideoMode(VideoMode))); - } -} - -void ProtoServer::newMessage(const proto::HyperionRequest * message) -{ - for (int i = 0; i < _proxy_connections.size(); ++i) - _proxy_connections.at(i)->sendMessage(*message); -} - -void ProtoServer::sendImageToProtoSlaves(int priority, const Image & image, int duration_ms) -{ - if ( _forwarder_enabled ) - { - for (int i = 0; i < _proxy_connections.size(); ++i) - _proxy_connections.at(i)->setImage(image, priority, duration_ms); - } -} - -void ProtoServer::componentStateChanged(const hyperion::Components component, bool enable) -{ - if (component == hyperion::COMP_FORWARDER) - { - if (_forwarder_enabled != enable) - { - _forwarder_enabled = enable; - Info(_log, "forwarder change state to %s", (_forwarder_enabled ? "enabled" : "disabled") ); - } - _hyperion->getComponentRegister().componentStateChanged(component, _forwarder_enabled); - } -} - -void ProtoServer::closedConnection(ProtoClientConnection *connection) -{ - Debug(_log, "Connection closed"); - _openConnections.remove(connection); - - // schedule to delete the connection object - connection->deleteLater(); -} diff --git a/libsrc/protoserver/message.proto b/libsrc/protoserver/message.proto deleted file mode 100644 index 9652453e..00000000 --- a/libsrc/protoserver/message.proto +++ /dev/null @@ -1,80 +0,0 @@ -package proto; - -message HyperionRequest { - enum Command { - COLOR = 1; - IMAGE = 2; - CLEAR = 3; - CLEARALL = 4; - } - - // command specification - required Command command = 1; - - // extensions to define all specific requests - extensions 10 to 100; -} - -message ColorRequest { - extend HyperionRequest { - optional ColorRequest colorRequest = 10; - } - - // priority to use when setting the color - required int32 priority = 1; - - // integer value containing the rgb color (0x00RRGGBB) - required int32 RgbColor = 2; - - // duration of the request (negative results in infinite) - optional int32 duration = 3; -} - -message ImageRequest { - extend HyperionRequest { - optional ImageRequest imageRequest = 11; - } - - // priority to use when setting the image - required int32 priority = 1; - - // width of the image - required int32 imagewidth = 2; - - // height of the image - required int32 imageheight = 3; - - // image data - required bytes imagedata = 4; - - // duration of the request (negative results in infinite) - optional int32 duration = 5; -} - -message ClearRequest { - extend HyperionRequest { - optional ClearRequest clearRequest = 12; - } - - // priority which need to be cleared - required int32 priority = 1; -} - -message HyperionReply { - enum Type { - REPLY = 1; - VIDEO = 2; - } - - // Identifies which field is filled in. - required Type type = 1; - - // flag indication success or failure - optional bool success = 2; - - // string indicating the reason for failure (if applicable) - optional string error = 3; - - // Proto Messages for video mode - optional int32 video = 4; -} diff --git a/libsrc/python/CMakeLists.txt b/libsrc/python/CMakeLists.txt new file mode 100644 index 00000000..cbe8f641 --- /dev/null +++ b/libsrc/python/CMakeLists.txt @@ -0,0 +1,21 @@ +find_package(PythonLibs 3.5 REQUIRED) + +# 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. +include_directories(${PYTHON_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS}/..) + +# 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} +) + +target_link_libraries(python + effectengine + hyperion-utils + ${PYTHON_LIBRARIES} +) diff --git a/libsrc/python/PythonInit.cpp b/libsrc/python/PythonInit.cpp new file mode 100644 index 00000000..f93f920f --- /dev/null +++ b/libsrc/python/PythonInit.cpp @@ -0,0 +1,31 @@ +#undef slots +#include +#define slots + +// utils +#include + +#include +#include + +// modules to init +#include + +PythonInit::PythonInit() +{ + // register modules + EffectModule::registerHyperionExtensionModule(); + + // init Python + Debug(Logger::getInstance("DAEMON"), "Initializing Python interpreter"); + Py_InitializeEx(0); + PyEval_InitThreads(); // Create the GIL + mainThreadState = PyEval_SaveThread(); +} + +PythonInit::~PythonInit() +{ + Debug(Logger::getInstance("DAEMON"), "Cleaning up Python interpreter"); + PyEval_RestoreThread(mainThreadState); + Py_Finalize(); +} diff --git a/libsrc/ssdp/CMakeLists.txt b/libsrc/ssdp/CMakeLists.txt new file mode 100644 index 00000000..939d59a7 --- /dev/null +++ b/libsrc/ssdp/CMakeLists.txt @@ -0,0 +1,14 @@ +# 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} +) + +target_link_libraries(ssdp + Qt5::Network + webserver +) diff --git a/libsrc/ssdp/SSDPDescription.h b/libsrc/ssdp/SSDPDescription.h new file mode 100644 index 00000000..70b37da6 --- /dev/null +++ b/libsrc/ssdp/SSDPDescription.h @@ -0,0 +1,41 @@ +#pragma once + +/// +/// The xml to fill with data +/// %1 base url http://192.168.0.177:80/ +/// %2 friendly name Hyperion 2.0.0 (192.168.0.177) +/// %3 modelNumber 2.0.0 +/// %4 serialNumber / UDN (H ID) Fjsa723dD0.... +/// +/// def URN urn:schemas-upnp-org:device:Basic:1 + +static const QString SSDP_DESCRIPTION = "" + "" + "" + "1" + "0" + "" + "%1" + "" + "urn:schemas-upnp-org:device:Basic:1" + "%2" + "Hyperion Open Source Ambient Lighting" + "https://www.hyperion-project.org" + "Hyperion Open Source Ambient Light" + "Hyperion" + "%3" + "https://www.hyperion-project.org" + "%4" + "uuid:%4" + "index.html" + "" + "" + "image/png" + "100" + "100" + "32" + "img/hyperion/ssdp_icon.png" + "" + "" + "" + ""; diff --git a/libsrc/ssdp/SSDPDiscover.cpp b/libsrc/ssdp/SSDPDiscover.cpp new file mode 100644 index 00000000..bfb6666f --- /dev/null +++ b/libsrc/ssdp/SSDPDiscover.cpp @@ -0,0 +1,169 @@ +#include + +// qt inc +#include +#include + +static const QHostAddress SSDP_ADDR("239.255.255.250"); +static const quint16 SSDP_PORT(1900); + +// as per upnp spec 1.1, section 1.2.2. +// TODO: Make IP and port below another #define and replace message below +static const QString UPNP_DISCOVER_MESSAGE = "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 1\r\n" + "ST: %1\r\n" + "\r\n"; + +SSDPDiscover::SSDPDiscover(QObject* parent) + : QObject(parent) + , _log(Logger::getInstance("SSDPDISCOVER")) + , _udpSocket(new QUdpSocket(this)) +{ + +} + +void SSDPDiscover::searchForService(const QString& st) +{ + _searchTarget = st; + _usnList.clear(); + // setup socket + connect(_udpSocket, &QUdpSocket::readyRead, this, &SSDPDiscover::readPendingDatagrams, Qt::UniqueConnection); + + sendSearch(st); +} + +const QString SSDPDiscover::getFirstService(const searchType& type, const QString& st, const int& timeout_ms) +{ + Info(_log, "Search for Hyperion server..."); + _searchTarget = st; + + // search + sendSearch(st); + + _udpSocket->waitForReadyRead(timeout_ms); + + while (_udpSocket->hasPendingDatagrams()) + { + QByteArray datagram; + datagram.resize(_udpSocket->pendingDatagramSize()); + QHostAddress sender; + quint16 senderPort; + + _udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); + + QString data(datagram); + QMap headers; + QString address; + // parse request + QStringList entries = data.split("\n", QString::SkipEmptyParts); + for(auto entry : entries) + { + // http header parse skip + if(entry.contains("HTTP/1.1")) + continue; + + // split into key:vale, be aware that value field may contain also a ":" + entry = entry.simplified(); + int pos = entry.indexOf(":"); + if(pos == -1) + continue; + + headers[entry.left(pos).trimmed().toLower()] = entry.mid(pos+1).trimmed(); + } + + // verify ssdp spec + if(!headers.contains("st")) + continue; + + // usn duplicates + if (_usnList.contains(headers.value("usn"))) + continue; + + if (headers.value("st") == _searchTarget) + { + _usnList << headers.value("usn"); + QUrl url(headers.value("location")); + //Info(_log, "Received msearch response from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st"))); + if(type == STY_WEBSERVER) + { + Info(_log, "Found Hyperion server at: %s:%d", QSTRING_CSTR(url.host()), url.port()); + + return url.host()+":"+QString::number(url.port()); + } + else if(type == STY_FLATBUFSERVER) + { + const QString fbsport = headers.value("hyperion-fbs-port"); + if(fbsport.isEmpty()) + { + continue; + } + else + { + Info(_log, "Found Hyperion server at: %s:%s", QSTRING_CSTR(url.host()), QSTRING_CSTR(fbsport)); + return url.host()+":"+fbsport; + } + } + } + } + Info(_log,"Search timeout, no Hyperion server found"); + return QString(); +} + +void SSDPDiscover::readPendingDatagrams() +{ + while (_udpSocket->hasPendingDatagrams()) { + + QByteArray datagram; + datagram.resize(_udpSocket->pendingDatagramSize()); + QHostAddress sender; + quint16 senderPort; + + _udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); + + QString data(datagram); + QMap headers; + // parse request + QStringList entries = data.split("\n", QString::SkipEmptyParts); + for(auto entry : entries) + { + // http header parse skip + if(entry.contains("HTTP/1.1")) + continue; + + // split into key:vale, be aware that value field may contain also a ":" + entry = entry.simplified(); + int pos = entry.indexOf(":"); + if(pos == -1) + continue; + + headers[entry.left(pos).trimmed().toLower()] = entry.mid(pos+1).trimmed(); + } + + // verify ssdp spec + if(!headers.contains("st")) + continue; + + // usn duplicates + if (_usnList.contains(headers.value("usn"))) + continue; + + if (headers.value("st") == _searchTarget) + { + _usnList << headers.value("usn"); + //Info(_log, "Received msearch response from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st"))); + QUrl url(headers.value("location")); + emit newService(url.host()+":"+QString::number(url.port())); + } + } +} + +void SSDPDiscover::sendSearch(const QString& st) +{ + const QString msg = UPNP_DISCOVER_MESSAGE.arg(st); + + _udpSocket->writeDatagram(msg.toUtf8(), + QHostAddress(SSDP_ADDR), + SSDP_PORT); +} diff --git a/libsrc/ssdp/SSDPHandler.cpp b/libsrc/ssdp/SSDPHandler.cpp new file mode 100644 index 00000000..9ce8a47f --- /dev/null +++ b/libsrc/ssdp/SSDPHandler.cpp @@ -0,0 +1,144 @@ +#include + +#include +#include "SSDPDescription.h" +#include +#include + +#include +#include + +SSDPHandler::SSDPHandler(WebServer* webserver, const quint16& flatBufPort, QObject * parent) + : SSDPServer(parent) + , _webserver(webserver) + , _localAddress() + , _NCA(nullptr) +{ + _flatbufPort = flatBufPort; + setFlatBufPort(_flatbufPort); +} + +void SSDPHandler::initServer() +{ + // prep server + SSDPServer::initServer(); + + _NCA = new QNetworkConfigurationManager(this); + + // listen for mSearchRequestes + connect(this, &SSDPServer::msearchRequestReceived, this, &SSDPHandler::handleMSearchRequest); + + connect(_NCA, &QNetworkConfigurationManager::configurationChanged, this, &SSDPHandler::handleNetworkConfigurationChanged); + + // get localAddress from interface + if(!getLocalAddress().isEmpty()) + { + _localAddress = getLocalAddress(); + } + + // startup if localAddress is found + if(!_localAddress.isEmpty() && _webserver->isInited()) + { + handleWebServerStateChange(true); + } +} + +void SSDPHandler::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::FLATBUFSERVER) + { + const QJsonObject& obj = config.object(); + if(obj["port"].toInt() != _flatbufPort) + { + _flatbufPort = obj["port"].toInt(); + setFlatBufPort(_flatbufPort); + } + } +} + +void SSDPHandler::handleWebServerStateChange(const bool newState) +{ + if(newState) + { + // refresh info + _webserver->setSSDPDescription(buildDesc()); + setDescriptionAddress(getDescAddress()); + if(start()) + { + sendAlive("upnp:rootdevice"); + sendAlive("urn:schemas-upnp-org:device:basic:1"); + sendAlive("urn:hyperion-project.org:device:basic:1"); + } + } + else + { + _webserver->setSSDPDescription(""); + stop(); + } +} + +void SSDPHandler::handleNetworkConfigurationChanged(const QNetworkConfiguration &config) +{ + // get localAddress from interface + if(!getLocalAddress().isEmpty()) + { + QString localAddress = getLocalAddress(); + if(_localAddress != localAddress) + { + // revoke old ip + sendByeBye("upnp:rootdevice"); + sendByeBye("urn:schemas-upnp-org:device:basic:1"); + sendByeBye("urn:hyperion-project.org:device:basic:1"); + + // update desc & notify new ip + _localAddress = localAddress; + _webserver->setSSDPDescription(buildDesc()); + setDescriptionAddress(getDescAddress()); + sendAlive("upnp:rootdevice"); + sendAlive("urn:schemas-upnp-org:device:basic:1"); + sendAlive("urn:hyperion-project.org:device:basic:1"); + } + } +} + +const QString SSDPHandler::getLocalAddress() +{ + // get the first valid IPv4 address. This is probably not that one we actually want to announce + for( const auto & address : QNetworkInterface::allAddresses()) + { + // is valid when, no loopback, IPv4 + if (!address.isLoopback() && address.protocol() == QAbstractSocket::IPv4Protocol ) + { + return address.toString(); + } + } + return QString(); +} + +void SSDPHandler::handleMSearchRequest(const QString& target, const QString& mx, const QString address, const quint16 & port) +{ + // TODO Response delay according to MX field (sec) random between 0 and MX + + // when searched for all devices / root devices / basic device + if(target == "ssdp:all" || target == "upnp:rootdevice" || target == "urn:schemas-upnp-org:device:basic:1" || target == "urn:hyperion-project.org:device:basic:1") + sendMSearchResponse(target, address, port); +} + +const QString SSDPHandler::getDescAddress() +{ + return getBaseAddress()+"description.xml"; +} + +const QString SSDPHandler::getBaseAddress() +{ + return "http://"+_localAddress+":"+QString::number(_webserver->getPort())+"/"; +} + +const QString SSDPHandler::buildDesc() +{ + /// %1 base url http://192.168.0.177:80/ + /// %2 friendly name Hyperion 2.0.0 (192.168.0.177) + /// %3 modelNumber 2.0.0 + /// %4 serialNumber / UDN (H ID) Fjsa723dD0.... + return SSDP_DESCRIPTION.arg(getBaseAddress(), QString("Hyperion (%2)").arg(_localAddress), QString(HYPERION_VERSION), Hyperion::getInstance()->getId()); +} diff --git a/libsrc/ssdp/SSDPServer.cpp b/libsrc/ssdp/SSDPServer.cpp new file mode 100644 index 00000000..01a586f0 --- /dev/null +++ b/libsrc/ssdp/SSDPServer.cpp @@ -0,0 +1,227 @@ +#include + +// util +#include +#include +#include + +#include +#include + +static const QHostAddress SSDP_ADDR("239.255.255.250"); +static const quint16 SSDP_PORT(1900); +static const QString SSDP_MAX_AGE("1800"); + +// as per upnp spec 1.1, section 1.2.2. +// - BOOTID.UPNP.ORG +// - CONFIGID.UPNP.ORG +// - SEARCHPORT.UPNP.ORG (optional) +// TODO: Make IP and port below another #define and replace message below +static const QString UPNP_ALIVE_MESSAGE = "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "CACHE-CONTROL: max-age=%1\r\n" + "LOCATION: %2\r\n" + "NT: %3\r\n" + "NTS: ssdp:alive\r\n" + "SERVER: %4\r\n" + "USN: uuid:%5\r\n" + "HYPERION-FBS-PORT: %6\r\n" + "\r\n"; + +// Implement ssdp:update as per spec 1.1, section 1.2.4 +// and use the below define to build the message, where +// SEARCHPORT.UPNP.ORG are optional. +// TODO: Make IP and port below another #define and replace message below +static const QString UPNP_UPDATE_MESSAGE = "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "LOCATION: %1\r\n" + "NT: %2\r\n" + "NTS: ssdp:update\r\n" + "USN: uuid:%3\r\n" +/* "CONFIGID.UPNP.ORG: %4\r\n" +UPNP spec = 1.1 "NEXTBOOTID.UPNP.ORG: %5\r\n" + "SEARCHPORT.UPNP.ORG: %6\r\n" +*/ "\r\n"; + +// TODO: Add this two fields commented below in the BYEBYE MESSAGE +// as per upnp spec 1.1, section 1.2.2 and 1.2.3. +// - BOOTID.UPNP.ORG +// - CONFIGID.UPNP.ORG +// TODO: Make IP and port below another #define and replace message below +static const QString UPNP_BYEBYE_MESSAGE = "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "NT: %1\r\n" + "NTS: ssdp:byebye\r\n" + "USN: uuid:%2\r\n" + "\r\n"; + +// TODO: Add this three fields commented below in the MSEARCH_RESPONSE +// as per upnp spec 1.1, section 1.3.3. +// - BOOTID.UPNP.ORG +// - CONFIGID.UPNP.ORG +// - SEARCHPORT.UPNP.ORG (optional) +static const QString UPNP_MSEARCH_RESPONSE = "HTTP/1.1 200 OK\r\n" + "CACHE-CONTROL: max-age = %1\r\n" + "DATE: %2\r\n" + "EXT: \r\n" + "LOCATION: %3\r\n" + "SERVER: %4\r\n" + "ST: %5\r\n" + "USN: uuid:%6\r\n" + "HYPERION-FBS-PORT: %7\r\n" + "\r\n"; + +SSDPServer::SSDPServer(QObject * parent) + : QObject(parent) + , _log(Logger::getInstance("SSDP")) + , _udpSocket(nullptr) + , _running(false) +{ + +} + +SSDPServer::~SSDPServer() +{ + stop(); +} + +void SSDPServer::initServer() +{ + _udpSocket = new QUdpSocket(this); + + // get system info + SysInfo::HyperionSysInfo data = SysInfo::get(); + + // create SERVER String + _serverHeader = data.prettyName+"/"+data.productVersion+" UPnP/1.0 Hyperion/"+QString(HYPERION_VERSION); + + // usn uuid + _uuid = Hyperion::getInstance()->getId(); + + connect(_udpSocket, &QUdpSocket::readyRead, this, &SSDPServer::readPendingDatagrams); +} + +const bool SSDPServer::start() +{ + if(!_running && _udpSocket->bind(QHostAddress::AnyIPv4, SSDP_PORT, QAbstractSocket::ShareAddress)) + { + _udpSocket->joinMulticastGroup(SSDP_ADDR); + _running = true; + return true; + } + return false; +} + +void SSDPServer::stop() +{ + if(_running) + { + // send BYEBYE Msg + sendByeBye("upnp:rootdevice"); + sendByeBye("urn:schemas-upnp-org:device:basic:1"); + sendByeBye("urn:hyperion-project.org:device:basic:1"); + _udpSocket->close(); + _running = false; + } +} + +void SSDPServer::readPendingDatagrams() +{ + while (_udpSocket->hasPendingDatagrams()) { + + QByteArray datagram; + datagram.resize(_udpSocket->pendingDatagramSize()); + QHostAddress sender; + quint16 senderPort; + + _udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); + + QString data(datagram); + QMap headers; + // parse request + QStringList entries = data.split("\n", QString::SkipEmptyParts); + for(auto entry : entries) + { + // http header parse skip + if(entry.contains("HTTP/1.1")) + continue; + + // split into key:vale, be aware that value field may contain also a ":" + entry = entry.simplified(); + int pos = entry.indexOf(":"); + if(pos == -1) + continue; + + headers[entry.left(pos).trimmed().toLower()] = entry.mid(pos+1).trimmed(); + } + + // verify ssdp spec + if(!headers.contains("man")) + continue; + + if (headers.value("man") == "\"ssdp:discover\"") + { + //Debug(_log, "Received msearch from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st"))); + emit msearchRequestReceived(headers.value("st"), headers.value("mx"), sender.toString(), senderPort); + } + } +} + +void SSDPServer::sendMSearchResponse(const QString& st, const QString& senderIp, const quint16& senderPort) +{ + QString message = UPNP_MSEARCH_RESPONSE.arg(SSDP_MAX_AGE + , QDateTime::currentDateTimeUtc().toString("ddd, dd MMM yyyy HH:mm:ss GMT") + , _descAddress + , _serverHeader + , st + , _uuid + , _fbsPort ); + + _udpSocket->writeDatagram(message.toUtf8(), + QHostAddress(senderIp), + senderPort); +} + +void SSDPServer::sendByeBye(const QString& st) +{ + QString message = UPNP_BYEBYE_MESSAGE.arg(st, _uuid+"::"+st ); + + // we repeat 3 times + quint8 rep = 0; + while(rep < 3) { + _udpSocket->writeDatagram(message.toUtf8(), + QHostAddress(SSDP_ADDR), + SSDP_PORT); + rep++; + } +} + +void SSDPServer::sendAlive(const QString& st) +{ + QString message = UPNP_ALIVE_MESSAGE.arg(SSDP_MAX_AGE + , _descAddress + , st + , _serverHeader + , _uuid+"::"+st + , _fbsPort); + + // we repeat 3 times + quint8 rep = 0; + while(rep < 3) { + _udpSocket->writeDatagram(message.toUtf8(), + QHostAddress(SSDP_ADDR), + SSDP_PORT); + rep++; + } +} + +void SSDPServer::sendUpdate(const QString& st) +{ + QString message = UPNP_UPDATE_MESSAGE.arg(_descAddress + , st + , _uuid+"::"+st ); + + _udpSocket->writeDatagram(message.toUtf8(), + QHostAddress(SSDP_ADDR), + SSDP_PORT); +} diff --git a/libsrc/udplistener/UDPListener.cpp b/libsrc/udplistener/UDPListener.cpp index cb53958c..60c4e3cb 100644 --- a/libsrc/udplistener/UDPListener.cpp +++ b/libsrc/udplistener/UDPListener.cpp @@ -1,33 +1,29 @@ // project includes #include -// hyperion util includes -#include "hyperion/ImageProcessorFactory.h" -#include "hyperion/ImageProcessor.h" -#include "utils/ColorRgb.h" +// bonjour includes +#include + +// hyperion includes #include "HyperionConfig.h" +// qt includes +#include +#include + using namespace hyperion; -UDPListener::UDPListener(const int priority, const int timeout, const QString& address, quint16 listenPort, bool shared) : +UDPListener::UDPListener(const QJsonDocument& config) : QObject(), - _hyperion(Hyperion::getInstance()), - _server(), - _openConnections(), - _priority(priority), - _timeout(timeout), + _server(new QUdpSocket(this)), + _priority(0), + _timeout(0), _log(Logger::getInstance("UDPLISTENER")), _isActive(false), - _listenPort(listenPort), - _bondage(shared ? QAbstractSocket::ShareAddress : QAbstractSocket::DefaultForPlatform) + _listenPort(0) { - _server = new QUdpSocket(this); - _listenAddress = address.isEmpty()? QHostAddress::AnyIPv4 : QHostAddress(address); - - // Set trigger for incoming connections - connect(_server, SIGNAL(readyRead()), this, SLOT(readPendingDatagrams())); - - _hyperion->registerPriority("UDPLISTENER", _priority); + // init + handleSettingsUpdate(settings::UDPLISTENER, config); } UDPListener::~UDPListener() @@ -35,7 +31,6 @@ UDPListener::~UDPListener() // clear the current channel stop(); delete _server; - _hyperion->clear(_priority); } @@ -51,7 +46,7 @@ void UDPListener::start() if (!_server->bind(_listenAddress, _listenPort, _bondage)) { - Warning(_log, "Could not bind to %s:%d", _listenAddress.toString().toStdString().c_str(), _listenPort); + Error(_log, "Could not bind to %s:%d", _listenAddress.toString().toStdString().c_str(), _listenPort); } else { @@ -62,7 +57,18 @@ void UDPListener::start() WarningIf( ! joinGroupOK, _log, "Multicast failed"); } _isActive = true; - emit statusChanged(_isActive); + + if(_serviceRegister == nullptr) + { + _serviceRegister = new BonjourServiceRegister(this); + _serviceRegister->registerService("_hyperiond-udp._udp", _listenPort); + } + else if( _serviceRegister->getPort() != _listenPort) + { + delete _serviceRegister; + _serviceRegister = new BonjourServiceRegister(this); + _serviceRegister->registerService("_hyperiond-udp._udp", _listenPort); + } } } @@ -73,7 +79,8 @@ void UDPListener::stop() _server->close(); _isActive = false; - emit statusChanged(_isActive); + Info(_log, "Stopped"); +// emit clearGlobalPriority(_priority, hyperion::COMP_UDPLISTENER); } void UDPListener::componentStateChanged(const hyperion::Components component, bool enable) @@ -84,9 +91,7 @@ void UDPListener::componentStateChanged(const hyperion::Components component, bo { if (enable) start(); else stop(); - Info(_log, "change state to %s", (enable ? "enabled" : "disabled") ); } - _hyperion->getComponentRegister().componentStateChanged(component, enable); } } @@ -105,9 +110,7 @@ void UDPListener::readPendingDatagrams() quint16 senderPort; _server->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); - processTheDatagram(&datagram, &sender); - } } @@ -115,20 +118,36 @@ void UDPListener::readPendingDatagrams() void UDPListener::processTheDatagram(const QByteArray * datagram, const QHostAddress * sender) { int packetLedCount = datagram->size()/3; - int hyperionLedCount = Hyperion::getInstance()->getLedCount(); - DebugIf( (packetLedCount != hyperionLedCount), _log, "packetLedCount (%d) != hyperionLedCount (%d)", packetLedCount, hyperionLedCount); + //DebugIf( (packetLedCount != hyperionLedCount), _log, "packetLedCount (%d) != hyperionLedCount (%d)", packetLedCount, hyperionLedCount); - std::vector _ledColors(Hyperion::getInstance()->getLedCount(), ColorRgb::BLACK); + std::vector _ledColors(packetLedCount, ColorRgb::BLACK); - for (int ledIndex=0; ledIndex < qMin(packetLedCount, hyperionLedCount); ledIndex++) { + for (int ledIndex=0; ledIndex < packetLedCount; ledIndex++) { ColorRgb & rgb = _ledColors[ledIndex]; rgb.red = datagram->at(ledIndex*3+0); rgb.green = datagram->at(ledIndex*3+1); rgb.blue = datagram->at(ledIndex*3+2); } - - _hyperion->setColors(_priority, _ledColors, _timeout, -1, hyperion::COMP_UDPLISTENER, sender->toString()); + // TODO provide a setInput with origin arg to overwrite senders smarter + emit registerGlobalInput(_priority, hyperion::COMP_UDPLISTENER, QString("UDPListener@%1").arg(sender->toString())); + emit setGlobalInput(_priority, _ledColors, _timeout); } +void UDPListener::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::UDPLISTENER) + { + QJsonObject obj = config.object(); + // if we change the prio we need to make sure the old one is cleared before we apply the new one! + stop(); - + QString addr = obj["address"].toString(""); + _priority = obj["priority"].toInt(); + _listenPort = obj["port"].toInt(); + _listenAddress = addr.isEmpty()? QHostAddress::AnyIPv4 : QHostAddress(addr); + _bondage = (obj["shared"].toBool(false)) ? QAbstractSocket::ShareAddress : QAbstractSocket::DefaultForPlatform; + _timeout = obj["timeout"].toInt(10000); + if(obj["enable"].toBool()) + start(); + } +} diff --git a/libsrc/utils/CMakeLists.txt b/libsrc/utils/CMakeLists.txt index a1483ec6..6b3865ac 100644 --- a/libsrc/utils/CMakeLists.txt +++ b/libsrc/utils/CMakeLists.txt @@ -9,11 +9,8 @@ if ( NOT ENABLE_PROFILER ) LIST ( REMOVE_ITEM Utils_SOURCES ${CURRENT_HEADER_DIR}/Profiler.h ${CURRENT_SOURCE_DIR}/Profiler.cpp ) endif() -set(Utils_RESOURCES ${CURRENT_SOURCE_DIR}/JSONRPC_schemas.qrc ) - add_library(hyperion-utils ${Utils_SOURCES} - ${Utils_RESOURCES} ) target_link_libraries(hyperion-utils diff --git a/libsrc/utils/FileUtils.cpp b/libsrc/utils/FileUtils.cpp index c7c6d208..31c70cd5 100644 --- a/libsrc/utils/FileUtils.cpp +++ b/libsrc/utils/FileUtils.cpp @@ -1,6 +1,7 @@ #include // qt incl +#include #include #include @@ -21,6 +22,17 @@ namespace FileUtils { return fi.path(); } + bool removeDir(const QString& path, Logger* log) + { + //QDir dir(path); + if(!QDir(path).removeRecursively()) + { + Error(log, "Failed to remove directory: %s", QSTRING_CSTR(path)); + return false; + } + return true; + } + bool fileExists(const QString& path, Logger* log, bool ignError) { QFile file(path); @@ -71,17 +83,18 @@ namespace FileUtils { return true; } - bool removeFile(const QString& path, Logger* log) + bool removeFile(const QString& path, Logger* log, bool ignError) { QFile file(path); if(!file.remove()) { - resolveFileError(file,log); + if(!ignError) + resolveFileError(file,log); return false; } return true; } - + QString convertPath(const QString path) { QString p = path; diff --git a/libsrc/utils/JsonUtils.cpp b/libsrc/utils/JsonUtils.cpp index d103978c..0040f241 100644 --- a/libsrc/utils/JsonUtils.cpp +++ b/libsrc/utils/JsonUtils.cpp @@ -9,8 +9,6 @@ #include #include -#include - namespace JsonUtils { bool readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) @@ -38,13 +36,33 @@ namespace JsonUtils { } bool parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) + { + QJsonDocument doc; + if(!parse(path, data, doc, log)) + return false; + + obj = doc.object(); + return true; + } + + bool parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log) + { + QJsonDocument doc; + if(!parse(path, data, doc, log)) + return false; + + arr = doc.array(); + return true; + } + + bool parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log) { //remove Comments in data QString cleanData = data; - cleanData.remove(QRegularExpression("([^:]?\\/\\/.*)")); + //cleanData .remove(QRegularExpression("([^:]?\\/\\/.*)")); QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(cleanData.toUtf8(), &error); + doc = QJsonDocument::fromJson(cleanData.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { @@ -63,7 +81,6 @@ namespace JsonUtils { Error(log,"Failed to parse json data from %s: Error: %s at Line: %i, Column: %i", QSTRING_CSTR(path), QSTRING_CSTR(error.errorString()), errorLine, errorColumn); return false; } - obj = doc.object(); return true; } @@ -74,6 +91,14 @@ namespace JsonUtils { if(!readFile(schemaPath, schema, log)) return false; + if(!validate(file, json, schema, log)) + return false; + return true; + + } + + bool validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log) + { QJsonSchemaChecker schemaChecker; schemaChecker.setSchema(schema); if (!schemaChecker.validate(json).first) @@ -120,7 +145,7 @@ namespace JsonUtils { obj.insert(attribute, resolveRefs(attributeValue.toObject(), obj, log)); else { - qDebug() <<"ADD ATTR:VALUE"< namespace Process { - + void restartHyperion(bool asNewProcess) { Logger* log = Logger::getInstance("Process"); @@ -19,8 +19,8 @@ void restartHyperion(bool asNewProcess) << " *******************************************" << std::endl << " * hyperion will restart now *" << std::endl << " *******************************************" << std::endl << std::endl; - - + + QStringList qargs = QCoreApplication::arguments(); int size = qargs.size(); char *args[size+1]; @@ -43,7 +43,7 @@ QByteArray command_exec(QString cmd, QByteArray data) QString result = ""; std::shared_ptr pipe(popen(cmd.toLocal8Bit().constData(), "r"), pclose); - if (pipe) + if (pipe) { while (!feof(pipe.get())) { @@ -54,4 +54,4 @@ QByteArray command_exec(QString cmd, QByteArray data) return QSTRING_CSTR(result); } -}; \ No newline at end of file +}; diff --git a/libsrc/utils/RgbChannelAdjustment.cpp b/libsrc/utils/RgbChannelAdjustment.cpp index dc763ff8..84d6d059 100644 --- a/libsrc/utils/RgbChannelAdjustment.cpp +++ b/libsrc/utils/RgbChannelAdjustment.cpp @@ -21,7 +21,7 @@ RgbChannelAdjustment::~RgbChannelAdjustment() void RgbChannelAdjustment::resetInitialized() { - Debug(_log, "initialize mapping with %d,%d,%d", _adjust[RED], _adjust[GREEN], _adjust[BLUE]); + //Debug(_log, "initialize mapping with %d,%d,%d", _adjust[RED], _adjust[GREEN], _adjust[BLUE]); memset(_initialized, false, sizeof(_initialized)); } diff --git a/libsrc/utils/RgbTransform.cpp b/libsrc/utils/RgbTransform.cpp index 2b3fd730..7c891dfc 100644 --- a/libsrc/utils/RgbTransform.cpp +++ b/libsrc/utils/RgbTransform.cpp @@ -144,8 +144,8 @@ void RgbTransform::transform(uint8_t & red, uint8_t & green, uint8_t & blue) { // apply gamma red = _mappingR[red]; - green = _mappingR[green]; - blue = _mappingR[blue]; + green = _mappingG[green]; + blue = _mappingB[blue]; // apply brightnesss int rgbSum = red+green+blue; diff --git a/libsrc/utils/Stats.cpp b/libsrc/utils/Stats.cpp index b78d6fc2..e31a5594 100644 --- a/libsrc/utils/Stats.cpp +++ b/libsrc/utils/Stats.cpp @@ -13,17 +13,21 @@ #include #include -Stats::Stats() +Stats* Stats::instance = nullptr; + +Stats::Stats(const QJsonObject& config) : QObject() , _log(Logger::getInstance("STATS")) , _hyperion(Hyperion::getInstance()) { + Stats::instance = this; + // generate hash foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) { if (!(interface.flags() & QNetworkInterface::IsLoopBack)) { - _hyperion->id = QString(QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit().append(_hyperion->getConfigFileName().toLocal8Bit()),QCryptographicHash::Sha1).toHex()); + _hyperion->setId(QString(QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit().append(_hyperion->getConfigFileName().toLocal8Bit()),QCryptographicHash::Sha1).toHex())); _hash = QString(QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex()); break; } @@ -34,35 +38,12 @@ Stats::Stats() { Warning(_log, "No interface found, abort"); // fallback id - _hyperion->id = QString(QCryptographicHash::hash(_hyperion->getConfigFileName().toLocal8Bit(),QCryptographicHash::Sha1).toHex()); + _hyperion->setId(QString(QCryptographicHash::hash(_hyperion->getConfigFileName().toLocal8Bit(),QCryptographicHash::Sha1).toHex())); return; } - // prepare content - QJsonObject config = _hyperion->getConfig(); - SysInfo::HyperionSysInfo data = SysInfo::get(); - - QJsonObject system; - system["kType" ] = data.kernelType; - system["arch" ] = data.architecture; - system["pType" ] = data.productType; - system["pVersion" ] = data.productVersion; - system["pName" ] = data.prettyName; - system["version" ] = QString(HYPERION_VERSION); - system["device" ] = LedDevice::activeDevice(); - system["id" ] = _hyperion->id; - system["hw_id" ] = _hash; - system["ledCount" ] = QString::number(Hyperion::getInstance()->getLedCount()); - system["comp_sm" ] = config["smoothing"].toObject().take("enable"); - system["comp_bb" ] = config["blackborderdetector"].toObject().take("enable"); - system["comp_fw" ] = config["forwarder"].toObject().take("enable"); - system["comp_udpl" ] = config["udpListener"].toObject().take("enable"); - system["comp_bobl" ] = config["boblightServer"].toObject().take("enable"); - system["comp_pc" ] = config["framegrabber"].toObject().take("enable"); - system["comp_uc" ] = config["grabberV4L2"].toArray().at(0).toObject().take("enable"); - - QJsonDocument doc(system); - _ba = doc.toJson(); + // prep data + handleDataUpdate(config); // QNetworkRequest Header _req.setRawHeader("Content-Type", "application/json"); @@ -75,13 +56,40 @@ Stats::Stats() connect(timer, SIGNAL(timeout()), this, SLOT(sendHTTP())); timer->start(604800000); - //delay initial check + // delay initial check QTimer::singleShot(60000, this, SLOT(initialExec())); } Stats::~Stats() { +} +void Stats::handleDataUpdate(const QJsonObject& config) +{ + // prepare content + SysInfo::HyperionSysInfo data = SysInfo::get(); + + QJsonObject system; + system["kType" ] = data.kernelType; + system["arch" ] = data.architecture; + system["pType" ] = data.productType; + system["pVersion" ] = data.productVersion; + system["pName" ] = data.prettyName; + system["version" ] = QString(HYPERION_VERSION); + system["device" ] = Hyperion::getInstance()->getActiveDevice(); + system["id" ] = _hyperion->getId(); + system["hw_id" ] = _hash; + system["ledCount" ] = QString::number(Hyperion::getInstance()->getLedCount()); + system["comp_sm" ] = config["smoothing"].toObject().take("enable"); + system["comp_bb" ] = config["blackborderdetector"].toObject().take("enable"); + system["comp_fw" ] = config["forwarder"].toObject().take("enable"); + system["comp_udpl" ] = config["udpListener"].toObject().take("enable"); + system["comp_bobl" ] = config["boblightServer"].toObject().take("enable"); + system["comp_pc" ] = config["framegrabber"].toObject().take("enable"); + system["comp_uc" ] = config["grabberV4L2"].toArray().at(0).toObject().take("enable"); + + QJsonDocument doc(system); + _ba = doc.toJson(); } void Stats::initialExec() @@ -100,7 +108,7 @@ void Stats::sendHTTP() void Stats::sendHTTPp() { - _req.setUrl(QUrl("https://api.hyperion-project.org/api/stats/"+_hyperion->id)); + _req.setUrl(QUrl("https://api.hyperion-project.org/api/stats/"+_hyperion->getId())); _mgr.put(_req,_ba); } @@ -122,7 +130,7 @@ bool Stats::trigger(bool set) { QString path = _hyperion->getRootPath()+"/misc/"; QDir dir; - QFile file(path + _hyperion->id); + QFile file(path + _hyperion->getId()); if(set && file.open(QIODevice::ReadWrite) ) { diff --git a/libsrc/webconfig/QtHttpServer.cpp b/libsrc/webconfig/QtHttpServer.cpp deleted file mode 100644 index 89207434..00000000 --- a/libsrc/webconfig/QtHttpServer.cpp +++ /dev/null @@ -1,133 +0,0 @@ - -#include "QtHttpServer.h" -#include "QtHttpRequest.h" -#include "QtHttpReply.h" -#include "QtHttpClientWrapper.h" - -#include - -const QString & QtHttpServer::HTTP_VERSION = QStringLiteral ("HTTP/1.1"); - -QtHttpServerWrapper::QtHttpServerWrapper (QObject * parent) - : QTcpServer (parent) - , m_useSsl (false) -{ } - -QtHttpServerWrapper::~QtHttpServerWrapper (void) { } - -void QtHttpServerWrapper::setUseSecure (const bool ssl) { - m_useSsl = ssl; -} - -void QtHttpServerWrapper::incomingConnection (qintptr handle) { - QTcpSocket * sock = (m_useSsl - ? new QSslSocket (this) - : new QTcpSocket (this)); - if (sock->setSocketDescriptor (handle)) { - addPendingConnection (sock); - } - else { - delete sock; - } -} - -QtHttpServer::QtHttpServer (QObject * parent) - : QObject (parent) - , m_useSsl (false) - , m_serverName (QStringLiteral ("The Qt5 HTTP Server")) -{ - m_sockServer = new QtHttpServerWrapper (this); - connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected); -} - -const QString & QtHttpServer::getServerName (void) const { - return m_serverName; -} - -quint16 QtHttpServer::getServerPort (void) const { - return m_sockServer->serverPort (); -} - -QString QtHttpServer::getErrorString (void) const { - return m_sockServer->errorString (); -} - -void QtHttpServer::start (quint16 port) { - if (m_sockServer->listen (QHostAddress::Any, port)) { - emit started (m_sockServer->serverPort ()); - } - else { - emit error (m_sockServer->errorString ()); - } -} - -void QtHttpServer::stop (void) { - if (m_sockServer->isListening ()) { - m_sockServer->close (); - emit stopped (); - } -} - -void QtHttpServer::setServerName (const QString & serverName) { - m_serverName = serverName; -} - -void QtHttpServer::setUseSecure (const bool ssl) { - m_useSsl = ssl; - m_sockServer->setUseSecure (m_useSsl); -} - -void QtHttpServer::setPrivateKey (const QSslKey & key) { - m_sslKey = key; -} - -void QtHttpServer::setCertificates (const QList & certs) { - m_sslCerts = certs; -} - -void QtHttpServer::onClientConnected (void) { - while (m_sockServer->hasPendingConnections ()) { - if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) { - connect (sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected); - if (m_useSsl) { - if (QSslSocket * ssl = qobject_cast (sock)) { - connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); - connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); - connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); - connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); - ssl->setLocalCertificateChain (m_sslCerts); - ssl->setPrivateKey (m_sslKey); - ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer); - ssl->startServerEncryption (); - } - } - QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, this); - m_socksClientsHash.insert (sock, wrapper); - emit clientConnected (wrapper->getGuid ()); - } - } -} - -void QtHttpServer::onClientSslEncrypted (void) { } - -void QtHttpServer::onClientSslPeerVerifyError (const QSslError & err) { - Q_UNUSED (err) -} - -void QtHttpServer::onClientSslErrors (const QList & errors) { - Q_UNUSED (errors) -} - -void QtHttpServer::onClientSslModeChanged (QSslSocket::SslMode mode) { - Q_UNUSED (mode) -} - -void QtHttpServer::onClientDisconnected (void) { - if (QTcpSocket * sockClient = qobject_cast (sender ())) { - if (QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR)) { - emit clientDisconnected (wrapper->getGuid ()); - wrapper->deleteLater (); - m_socksClientsHash.remove (sockClient); - } - } -} diff --git a/libsrc/webconfig/QtHttpServer.h b/libsrc/webconfig/QtHttpServer.h deleted file mode 100644 index a14b191e..00000000 --- a/libsrc/webconfig/QtHttpServer.h +++ /dev/null @@ -1,86 +0,0 @@ -#ifndef QTHTTPSERVER_H -#define QTHTTPSERVER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class QTcpSocket; -class QTcpServer; - -class QtHttpRequest; -class QtHttpReply; -class QtHttpClientWrapper; - -class QtHttpServerWrapper : public QTcpServer { - Q_OBJECT - -public: - explicit QtHttpServerWrapper (QObject * parent = Q_NULLPTR); - virtual ~QtHttpServerWrapper (void); - - void setUseSecure (const bool ssl = true); - -protected: - void incomingConnection (qintptr handle) Q_DECL_OVERRIDE; - -private: - bool m_useSsl; -}; - -class QtHttpServer : public QObject { - Q_OBJECT - -public: - explicit QtHttpServer (QObject * parent = Q_NULLPTR); - - static const QString & HTTP_VERSION; - - typedef void (QSslSocket::* SslErrorSignal) (const QList &); - - const QString & getServerName (void) const; - - quint16 getServerPort (void) const; - QString getErrorString (void) const; - -public slots: - void start (quint16 port = 0); - void stop (void); - void setServerName (const QString & serverName); - void setUseSecure (const bool ssl = true); - void setPrivateKey (const QSslKey & key); - void setCertificates (const QList & certs); - -signals: - void started (quint16 port); - void stopped (void); - void error (const QString & msg); - void clientConnected (const QString & guid); - void clientDisconnected (const QString & guid); - void requestNeedsReply (QtHttpRequest * request, QtHttpReply * reply); - -private slots: - void onClientConnected (void); - void onClientDisconnected (void); - void onClientSslEncrypted (void); - void onClientSslPeerVerifyError (const QSslError & err); - void onClientSslErrors (const QList & errors); - void onClientSslModeChanged (QSslSocket::SslMode mode); - -private: - bool m_useSsl; - QSslKey m_sslKey; - QList m_sslCerts; - QString m_serverName; - QtHttpServerWrapper * m_sockServer; - QHash m_socksClientsHash; -}; - -#endif // QTHTTPSERVER_H - diff --git a/libsrc/webconfig/WebConfig.cpp b/libsrc/webconfig/WebConfig.cpp deleted file mode 100644 index 36c9cf71..00000000 --- a/libsrc/webconfig/WebConfig.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "webconfig/WebConfig.h" -#include "StaticFileServing.h" - -#include - -WebConfig::WebConfig(QObject * parent) - : QObject(parent) - , _hyperion(Hyperion::getInstance()) - , _server(nullptr) -{ - Logger* log = Logger::getInstance("WEBSERVER"); - _port = WEBCONFIG_DEFAULT_PORT; - _baseUrl = WEBCONFIG_DEFAULT_PATH; - const QJsonObject config = _hyperion->getQJsonConfig(); - - bool webconfigEnable = true; - - if (config.contains("webConfig")) - { - const QJsonObject webconfigConfig = config["webConfig"].toObject(); - webconfigEnable = webconfigConfig["enable"].toBool(true); - _port = webconfigConfig["port"].toInt(_port); - _baseUrl = webconfigConfig["document_root"].toString(_baseUrl); - } - - if ( (_baseUrl != ":/webconfig") && !_baseUrl.trimmed().isEmpty()) - { - QFileInfo info(_baseUrl); - if (!info.exists() || !info.isDir()) - { - Error(log, "document_root '%s' is invalid, set to default '%s'", _baseUrl.toUtf8().constData(), WEBCONFIG_DEFAULT_PATH.toUtf8().constData()); - _baseUrl = WEBCONFIG_DEFAULT_PATH; - } - } - else - _baseUrl = WEBCONFIG_DEFAULT_PATH; - - Debug(log, "WebUI initialized, document root: %s", _baseUrl.toUtf8().constData()); - if ( webconfigEnable ) - { - start(); - } -} - - -WebConfig::~WebConfig() -{ - stop(); -} - - -void WebConfig::start() -{ - if ( _server == nullptr ) - _server = new StaticFileServing (_hyperion, _baseUrl, _port, this); -} - -void WebConfig::stop() -{ - if ( _server != nullptr ) - { - delete _server; - _server = nullptr; - } -} - - diff --git a/libsrc/webconfig/CMakeLists.txt b/libsrc/webserver/CMakeLists.txt similarity index 78% rename from libsrc/webconfig/CMakeLists.txt rename to libsrc/webserver/CMakeLists.txt index 51e99367..175df60e 100644 --- a/libsrc/webconfig/CMakeLists.txt +++ b/libsrc/webserver/CMakeLists.txt @@ -1,7 +1,7 @@ # Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/webconfig) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/webconfig) +set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/webserver) +set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/webserver) 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/* ) @@ -13,13 +13,14 @@ ENDFOREACH() CONFIGURE_FILE(${CURRENT_SOURCE_DIR}/WebConfig.qrc.in ${CMAKE_BINARY_DIR}/WebConfig.qrc ) SET(WebConfig_RESOURCES ${CMAKE_BINARY_DIR}/WebConfig.qrc) -add_library(webconfig +add_library(webserver ${WebConfig_SOURCES} ${WebConfig_RESOURCES} ) -target_link_libraries(webconfig +target_link_libraries(webserver hyperion hyperion-utils + hyperion-api Qt5::Network ) diff --git a/libsrc/webconfig/CgiHandler.cpp b/libsrc/webserver/CgiHandler.cpp similarity index 85% rename from libsrc/webconfig/CgiHandler.cpp rename to libsrc/webserver/CgiHandler.cpp index e4e5d404..1824397c 100644 --- a/libsrc/webconfig/CgiHandler.cpp +++ b/libsrc/webserver/CgiHandler.cpp @@ -13,12 +13,10 @@ #include #include -CgiHandler::CgiHandler (Hyperion * hyperion, QString baseUrl, QObject * parent) +CgiHandler::CgiHandler (QObject * parent) : QObject(parent) - , _hyperion(hyperion) , _args(QStringList()) - , _hyperionConfig(_hyperion->getQJsonConfig()) - , _baseUrl(baseUrl) + , _baseUrl() , _log(Logger::getInstance("WEBSERVER")) { } @@ -27,6 +25,11 @@ CgiHandler::~CgiHandler() { } +void CgiHandler::setBaseUrl(const QString& url) +{ + _baseUrl = url; +} + void CgiHandler::exec(const QStringList & args, QtHttpRequest * request, QtHttpReply * reply) { try @@ -52,11 +55,6 @@ void CgiHandler::cmd_cfg_jsonserver() if ( _args.at(0) == "cfg_jsonserver" ) { quint16 jsonPort = 19444; - if (_hyperionConfig.contains("jsonServer")) - { - const QJsonObject jsonConfig = _hyperionConfig["jsonServer"].toObject(); - jsonPort = jsonConfig["port"].toInt(jsonPort); - } // send result as reply _reply->addHeader ("Content-Type", "text/plain" ); diff --git a/libsrc/webconfig/CgiHandler.h b/libsrc/webserver/CgiHandler.h similarity index 72% rename from libsrc/webconfig/CgiHandler.h rename to libsrc/webserver/CgiHandler.h index 02c5aa6a..e48d9577 100644 --- a/libsrc/webconfig/CgiHandler.h +++ b/libsrc/webserver/CgiHandler.h @@ -5,7 +5,6 @@ #include #include -#include #include #include "QtHttpReply.h" @@ -15,25 +14,22 @@ class CgiHandler : public QObject { Q_OBJECT public: - CgiHandler (Hyperion * hyperion, QString baseUrl, QObject * parent = NULL); + CgiHandler (QObject * parent = NULL); virtual ~CgiHandler (void); + void setBaseUrl(const QString& url); void exec(const QStringList & args,QtHttpRequest * request, QtHttpReply * reply); - + // cgi commands void cmd_cfg_jsonserver(); void cmd_runscript (); - + private: - Hyperion* _hyperion; QtHttpReply * _reply; QtHttpRequest * _request; QStringList _args; - const QJsonObject & _hyperionConfig; - const QString _baseUrl; + QString _baseUrl; Logger * _log; }; #endif // CGIHANDLER_H - - diff --git a/libsrc/webconfig/QtHttpClientWrapper.cpp b/libsrc/webserver/QtHttpClientWrapper.cpp similarity index 97% rename from libsrc/webconfig/QtHttpClientWrapper.cpp rename to libsrc/webserver/QtHttpClientWrapper.cpp index 0720abe5..04e22be3 100644 --- a/libsrc/webconfig/QtHttpClientWrapper.cpp +++ b/libsrc/webserver/QtHttpClientWrapper.cpp @@ -4,8 +4,8 @@ #include "QtHttpReply.h" #include "QtHttpServer.h" #include "QtHttpHeader.h" -#include "WebSocketClient.h" #include "WebJsonRpc.h" +#include "webserver/WebSocketClient.h" #include #include @@ -23,6 +23,8 @@ QtHttpClientWrapper::QtHttpClientWrapper (QTcpSocket * sock, QtHttpServer * pare , m_sockClient (sock) , m_currentRequest (Q_NULLPTR) , m_serverHandle (parent) + , m_websocketClient(nullptr) + , m_webJsonRpc (nullptr) { connect (m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); } @@ -118,7 +120,7 @@ void QtHttpClientWrapper::onClientDataReceived (void) { { // disconnect this slot from socket for further requests disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); - m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, this); + m_websocketClient = new WebSocketClient(m_currentRequest->getHeader(QtHttpHeader::SecWebSocketKey), m_sockClient, this); } break; } @@ -159,9 +161,8 @@ void QtHttpClientWrapper::onClientDataReceived (void) { 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); + m_parsingStatus = sendReplyToClient (&reply); break; } case ParsingError: { // there was an error durin one of parsing steps diff --git a/libsrc/webconfig/QtHttpClientWrapper.h b/libsrc/webserver/QtHttpClientWrapper.h similarity index 93% rename from libsrc/webconfig/QtHttpClientWrapper.h rename to libsrc/webserver/QtHttpClientWrapper.h index 3e2c3a35..13a2438d 100644 --- a/libsrc/webconfig/QtHttpClientWrapper.h +++ b/libsrc/webserver/QtHttpClientWrapper.h @@ -50,8 +50,8 @@ private: QTcpSocket * m_sockClient; QtHttpRequest * m_currentRequest; QtHttpServer * m_serverHandle; - WebSocketClient * m_websocketClient = nullptr; - WebJsonRpc * m_webJsonRpc = nullptr; + WebSocketClient * m_websocketClient; + WebJsonRpc * m_webJsonRpc; }; #endif // QTHTTPCLIENTWRAPPER_H diff --git a/libsrc/webconfig/QtHttpHeader.cpp b/libsrc/webserver/QtHttpHeader.cpp similarity index 100% rename from libsrc/webconfig/QtHttpHeader.cpp rename to libsrc/webserver/QtHttpHeader.cpp diff --git a/libsrc/webconfig/QtHttpHeader.h b/libsrc/webserver/QtHttpHeader.h similarity index 100% rename from libsrc/webconfig/QtHttpHeader.h rename to libsrc/webserver/QtHttpHeader.h diff --git a/libsrc/webconfig/QtHttpReply.cpp b/libsrc/webserver/QtHttpReply.cpp similarity index 100% rename from libsrc/webconfig/QtHttpReply.cpp rename to libsrc/webserver/QtHttpReply.cpp diff --git a/libsrc/webconfig/QtHttpReply.h b/libsrc/webserver/QtHttpReply.h similarity index 100% rename from libsrc/webconfig/QtHttpReply.h rename to libsrc/webserver/QtHttpReply.h diff --git a/libsrc/webconfig/QtHttpRequest.cpp b/libsrc/webserver/QtHttpRequest.cpp similarity index 100% rename from libsrc/webconfig/QtHttpRequest.cpp rename to libsrc/webserver/QtHttpRequest.cpp diff --git a/libsrc/webconfig/QtHttpRequest.h b/libsrc/webserver/QtHttpRequest.h similarity index 100% rename from libsrc/webconfig/QtHttpRequest.h rename to libsrc/webserver/QtHttpRequest.h diff --git a/libsrc/webserver/QtHttpServer.cpp b/libsrc/webserver/QtHttpServer.cpp new file mode 100644 index 00000000..4a118f24 --- /dev/null +++ b/libsrc/webserver/QtHttpServer.cpp @@ -0,0 +1,162 @@ +#include "QtHttpServer.h" +#include "QtHttpRequest.h" +#include "QtHttpReply.h" +#include "QtHttpClientWrapper.h" + +#include + +const QString & QtHttpServer::HTTP_VERSION = QStringLiteral ("HTTP/1.1"); + +QtHttpServerWrapper::QtHttpServerWrapper (QObject * parent) + : QTcpServer (parent) + , m_useSsl (false) +{ +} + +QtHttpServerWrapper::~QtHttpServerWrapper (void) +{ +} + +void QtHttpServerWrapper::setUseSecure (const bool ssl) { + m_useSsl = ssl; +} + +void QtHttpServerWrapper::incomingConnection (qintptr handle) +{ + QTcpSocket * sock = (m_useSsl + ? new QSslSocket (this) + : new QTcpSocket (this)); + (sock->setSocketDescriptor (handle)) + ? addPendingConnection (sock) + : delete sock; +} + +QtHttpServer::QtHttpServer (QObject * parent) + : QObject (parent) + , m_useSsl (false) + , m_serverName (QStringLiteral ("The Qt5 HTTP Server")) +{ + m_sockServer = new QtHttpServerWrapper (this); + connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected); +} + +const QString & QtHttpServer::getServerName (void) const +{ + return m_serverName; +} + +quint16 QtHttpServer::getServerPort (void) const +{ + return m_sockServer->serverPort (); +} + +QString QtHttpServer::getErrorString (void) const +{ + return m_sockServer->errorString (); +} + +void QtHttpServer::start (quint16 port) +{ + if(!m_sockServer->isListening()) + (m_sockServer->listen (QHostAddress::Any, port)) + ? emit started (m_sockServer->serverPort ()) + : emit error (m_sockServer->errorString ()); +} + +void QtHttpServer::stop (void) +{ + if (m_sockServer->isListening ()) + { + m_sockServer->close (); + + // disconnect clients + const QList socks = m_socksClientsHash.keys(); + for(auto sock : socks) + { + sock->close(); + } + + emit stopped (); + } +} + +void QtHttpServer::setServerName (const QString & serverName) +{ + m_serverName = serverName; +} + +void QtHttpServer::setUseSecure (const bool ssl) +{ + m_useSsl = ssl; + m_sockServer->setUseSecure (m_useSsl); +} + +void QtHttpServer::setPrivateKey (const QSslKey & key) +{ + m_sslKey = key; +} + +void QtHttpServer::setCertificates (const QList & certs) +{ + m_sslCerts = certs; +} + +void QtHttpServer::onClientConnected (void) +{ + while (m_sockServer->hasPendingConnections ()) + { + if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) + { + connect (sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected); + if (m_useSsl) + { + if (QSslSocket * ssl = qobject_cast (sock)) + { + connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); + connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); + connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); + connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); + ssl->setLocalCertificateChain (m_sslCerts); + ssl->setPrivateKey (m_sslKey); + ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer); + ssl->startServerEncryption (); + } + } + QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, this); + m_socksClientsHash.insert (sock, wrapper); + emit clientConnected (wrapper->getGuid ()); + } + } +} + +void QtHttpServer::onClientSslEncrypted (void) +{ +} + +void QtHttpServer::onClientSslPeerVerifyError (const QSslError & err) +{ + Q_UNUSED (err) +} + +void QtHttpServer::onClientSslErrors (const QList & errors) +{ + Q_UNUSED (errors) +} + +void QtHttpServer::onClientSslModeChanged (QSslSocket::SslMode mode) +{ + Q_UNUSED (mode) +} + +void QtHttpServer::onClientDisconnected (void) +{ + if (QTcpSocket * sockClient = qobject_cast (sender ())) + { + if (QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR)) + { + emit clientDisconnected (wrapper->getGuid ()); + wrapper->deleteLater (); + m_socksClientsHash.remove (sockClient); + } + } +} diff --git a/libsrc/webserver/QtHttpServer.h b/libsrc/webserver/QtHttpServer.h new file mode 100644 index 00000000..6a949e64 --- /dev/null +++ b/libsrc/webserver/QtHttpServer.h @@ -0,0 +1,88 @@ +#ifndef QTHTTPSERVER_H +#define QTHTTPSERVER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QTcpSocket; +class QTcpServer; + +class QtHttpRequest; +class QtHttpReply; +class QtHttpClientWrapper; + +class QtHttpServerWrapper : public QTcpServer { + Q_OBJECT + +public: + explicit QtHttpServerWrapper (QObject * parent = Q_NULLPTR); + virtual ~QtHttpServerWrapper (void); + + void setUseSecure (const bool ssl = true); + +protected: + void incomingConnection (qintptr handle) Q_DECL_OVERRIDE; + +private: + bool m_useSsl; +}; + +class QtHttpServer : public QObject { + Q_OBJECT + +public: + explicit QtHttpServer (QObject * parent = Q_NULLPTR); + + static const QString & HTTP_VERSION; + + typedef void (QSslSocket::* SslErrorSignal) (const QList &); + + const QString & getServerName (void) const; + + quint16 getServerPort (void) const; + QString getErrorString (void) const; + + const bool isListening(void) { return m_sockServer->isListening(); }; + +public slots: + void start (quint16 port = 0); + void stop (void); + void setServerName (const QString & serverName); + void setUseSecure (const bool ssl = true); + void setPrivateKey (const QSslKey & key); + void setCertificates (const QList & certs); + +signals: + void started (quint16 port); + void stopped (void); + void error (const QString & msg); + void clientConnected (const QString & guid); + void clientDisconnected (const QString & guid); + void requestNeedsReply (QtHttpRequest * request, QtHttpReply * reply); + +private slots: + void onClientConnected (void); + void onClientDisconnected (void); + void onClientSslEncrypted (void); + void onClientSslPeerVerifyError (const QSslError & err); + void onClientSslErrors (const QList & errors); + void onClientSslModeChanged (QSslSocket::SslMode mode); + +private: + bool m_useSsl; + QSslKey m_sslKey; + QList m_sslCerts; + QString m_serverName; + QtHttpServerWrapper * m_sockServer; + QHash m_socksClientsHash; +}; + +#endif // QTHTTPSERVER_H + diff --git a/libsrc/webconfig/StaticFileServing.cpp b/libsrc/webserver/StaticFileServing.cpp similarity index 52% rename from libsrc/webconfig/StaticFileServing.cpp rename to libsrc/webserver/StaticFileServing.cpp index 80e8e901..ad260374 100644 --- a/libsrc/webconfig/StaticFileServing.cpp +++ b/libsrc/webserver/StaticFileServing.cpp @@ -8,66 +8,36 @@ #include #include #include -#include -#include -#include -#include #include -StaticFileServing::StaticFileServing (Hyperion *hyperion, QString baseUrl, quint16 port, QObject * parent) +StaticFileServing::StaticFileServing (QObject * parent) : QObject (parent) - , _hyperion(hyperion) - , _baseUrl (baseUrl) - , _cgi(hyperion, baseUrl, this) + , _baseUrl () + , _cgi(this) , _log(Logger::getInstance("WEBSERVER")) { Q_INIT_RESOURCE(WebConfig); _mimeDb = new QMimeDatabase; - - _server = new QtHttpServer (this); - _server->setServerName (QStringLiteral ("Hyperion WebConfig")); - - connect (_server, &QtHttpServer::started, this, &StaticFileServing::onServerStarted); - connect (_server, &QtHttpServer::stopped, this, &StaticFileServing::onServerStopped); - connect (_server, &QtHttpServer::error, this, &StaticFileServing::onServerError); - connect (_server, &QtHttpServer::requestNeedsReply, this, &StaticFileServing::onRequestNeedsReply); - - _server->start (port); } StaticFileServing::~StaticFileServing () { - _server->stop (); + } -void StaticFileServing::onServerStarted (quint16 port) +void StaticFileServing::setBaseUrl(const QString& url) { - Info(_log, "started on port %d name '%s'", port ,_server->getServerName().toStdString().c_str()); - const QJsonObject & generalConfig = _hyperion->getQJsonConfig()["general"].toObject(); - const QString mDNSDescr = generalConfig["name"].toString("") + "@" + QHostInfo::localHostName() + ":" + QString::number(port); - - // txt record for zeroconf - QString id = _hyperion->id; - std::string version = HYPERION_VERSION; - std::vector > txtRecord = {{"id",id.toStdString()},{"version",version}}; - - BonjourServiceRegister *bonjourRegister_http = new BonjourServiceRegister(); - bonjourRegister_http->registerService( - BonjourRecord(mDNSDescr, "_hyperiond-http._tcp", QString()), - port, - txtRecord - ); - Debug(_log, "Web Config mDNS responder started"); + _baseUrl = url; + _cgi.setBaseUrl(url); } -void StaticFileServing::onServerStopped () { - Info(_log, "stopped %s", _server->getServerName().toStdString().c_str()); -} - -void StaticFileServing::onServerError (QString msg) +void StaticFileServing::setSSDPDescription(const QString& desc) { - Error(_log, "%s", msg.toStdString().c_str()); + if(desc.isEmpty()) + _ssdpDescription.clear(); + else + _ssdpDescription = desc.toLocal8Bit(); } void StaticFileServing::printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, QString errorMessage) @@ -114,26 +84,34 @@ void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpRepl QStringList uri_parts = path.split('/', QString::SkipEmptyParts); // special uri handling for server commands - if ( ! uri_parts.empty() && uri_parts.at(0) == "cgi" ) + if ( ! uri_parts.empty() ) { - uri_parts.removeAt(0); - try + if(uri_parts.at(0) == "cgi") { - _cgi.exec(uri_parts, request, reply); + uri_parts.removeAt(0); + try + { + _cgi.exec(uri_parts, request, reply); + } + catch(int err) + { + Error(_log,"Exception while executing cgi %s : %d", path.toStdString().c_str(), err); + printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); + } + catch(std::exception &e) + { + Error(_log,"Exception while executing cgi %s : %s", path.toStdString().c_str(), e.what()); + printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); + } + return; } - catch(int err) + else if(uri_parts.at(0) == "description.xml" && !_ssdpDescription.isNull()) { - Error(_log,"Exception while executing cgi %s : %d", path.toStdString().c_str(), err); - printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); + reply->addHeader ("Content-Type", "text/xml"); + reply->appendRawData (_ssdpDescription); + return; } - catch(std::exception &e) - { - Error(_log,"Exception while executing cgi %s : %s", path.toStdString().c_str(), e.what()); - printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); - } - return; } - Q_INIT_RESOURCE(WebConfig); QFileInfo info(_baseUrl % "/" % path); if ( path == "/" || path.isEmpty() ) diff --git a/libsrc/webconfig/StaticFileServing.h b/libsrc/webserver/StaticFileServing.h similarity index 61% rename from libsrc/webconfig/StaticFileServing.h rename to libsrc/webserver/StaticFileServing.h index 42e03950..c44f1a65 100644 --- a/libsrc/webconfig/StaticFileServing.h +++ b/libsrc/webserver/StaticFileServing.h @@ -1,38 +1,42 @@ #ifndef STATICFILESERVING_H #define STATICFILESERVING_H -#include #include -#include "QtHttpServer.h" +//#include "QtHttpServer.h" #include "QtHttpRequest.h" #include "QtHttpReply.h" #include "QtHttpHeader.h" #include "CgiHandler.h" -#include #include class StaticFileServing : public QObject { Q_OBJECT public: - explicit StaticFileServing (Hyperion *hyperion, QString baseUrl, quint16 port, QObject * parent = nullptr); + explicit StaticFileServing (QObject * parent = nullptr); virtual ~StaticFileServing (void); + /// + /// @brief Overwrite current base url + /// + void setBaseUrl(const QString& url); + /// + /// @brief Set a new SSDP description, if empty the description will be unset and clients will get a NotFound + /// @param The description + /// + void setSSDPDescription(const QString& desc); + public slots: - void onServerStopped (void); - void onServerStarted (quint16 port); - void onServerError (QString msg); void onRequestNeedsReply (QtHttpRequest * request, QtHttpReply * reply); private: - Hyperion * _hyperion; QString _baseUrl; - QtHttpServer * _server; QMimeDatabase * _mimeDb; CgiHandler _cgi; Logger * _log; + QByteArray _ssdpDescription; void printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, QString errorMessage); diff --git a/libsrc/webconfig/WebConfig.qrc.in b/libsrc/webserver/WebConfig.qrc.in similarity index 100% rename from libsrc/webconfig/WebConfig.qrc.in rename to libsrc/webserver/WebConfig.qrc.in diff --git a/libsrc/webconfig/WebJsonRpc.cpp b/libsrc/webserver/WebJsonRpc.cpp similarity index 74% rename from libsrc/webconfig/WebJsonRpc.cpp rename to libsrc/webserver/WebJsonRpc.cpp index 16f4a52b..85e16fa0 100644 --- a/libsrc/webconfig/WebJsonRpc.cpp +++ b/libsrc/webserver/WebJsonRpc.cpp @@ -4,7 +4,7 @@ #include "QtHttpServer.h" #include "QtHttpClientWrapper.h" -#include +#include WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClientWrapper* parent) : QObject(parent) @@ -13,20 +13,20 @@ WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClien , _log(Logger::getInstance("HTTPJSONRPC")) { const QString client = request->getClientInfo().clientAddress.toString(); - _jsonProcessor = new JsonProcessor(client, _log, this, true); - connect(_jsonProcessor, &JsonProcessor::callbackMessage, this, &WebJsonRpc::handleCallback); + _jsonAPI = new JsonAPI(client, _log, this, true); + connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebJsonRpc::handleCallback); } void WebJsonRpc::handleMessage(QtHttpRequest* request) { QByteArray data = request->getRawData(); _unlocked = true; - _jsonProcessor->handleMessage(data); + _jsonAPI->handleMessage(data); } void WebJsonRpc::handleCallback(QJsonObject obj) { - // guard against wrong callbacks; TODO: Remove when JsonProcessor is more solid + // guard against wrong callbacks; TODO: Remove when JSONAPI is more solid if(!_unlocked) return; _unlocked = false; // construct reply with headers timestamp and server name diff --git a/libsrc/webconfig/WebJsonRpc.h b/libsrc/webserver/WebJsonRpc.h similarity index 83% rename from libsrc/webconfig/WebJsonRpc.h rename to libsrc/webserver/WebJsonRpc.h index 5aabfa71..06adb33b 100644 --- a/libsrc/webconfig/WebJsonRpc.h +++ b/libsrc/webserver/WebJsonRpc.h @@ -1,11 +1,15 @@ #pragma once +// utils includes #include +// qt includes +#include + class QtHttpServer; class QtHttpRequest; class QtHttpClientWrapper; -class JsonProcessor; +class JsonAPI; class WebJsonRpc : public QObject { Q_OBJECT @@ -18,7 +22,7 @@ private: QtHttpServer* _server; QtHttpClientWrapper* _wrapper; Logger* _log; - JsonProcessor* _jsonProcessor; + JsonAPI* _jsonAPI; bool _unlocked = false; diff --git a/libsrc/webserver/WebServer.cpp b/libsrc/webserver/WebServer.cpp new file mode 100644 index 00000000..c570736a --- /dev/null +++ b/libsrc/webserver/WebServer.cpp @@ -0,0 +1,127 @@ +#include "webserver/WebServer.h" +#include "StaticFileServing.h" +#include "QtHttpServer.h" + +#include +#include + +// bonjour +#include + +// netUtil +#include + + +WebServer::WebServer(const QJsonDocument& config, QObject * parent) + : QObject(parent) + , _config(config) + , _log(Logger::getInstance("WEBSERVER")) + , _server() +{ +} + +WebServer::~WebServer() +{ + stop(); +} + +void WebServer::initServer() +{ + _server = new QtHttpServer (this); + _server->setServerName (QStringLiteral ("Hyperion Webserver")); + + connect (_server, &QtHttpServer::started, this, &WebServer::onServerStarted); + connect (_server, &QtHttpServer::stopped, this, &WebServer::onServerStopped); + connect (_server, &QtHttpServer::error, this, &WebServer::onServerError); + + // create StaticFileServing + _staticFileServing = new StaticFileServing (this); + connect(_server, &QtHttpServer::requestNeedsReply, _staticFileServing, &StaticFileServing::onRequestNeedsReply); + + // init + handleSettingsUpdate(settings::WEBSERVER, _config); +} + +void WebServer::onServerStarted (quint16 port) +{ + _inited= true; + + Info(_log, "Started on port %d name '%s'", port ,_server->getServerName().toStdString().c_str()); + + if(_serviceRegister == nullptr) + { + _serviceRegister = new BonjourServiceRegister(this); + _serviceRegister->registerService("_hyperiond-http._tcp", port); + } + else if( _serviceRegister->getPort() != port) + { + delete _serviceRegister; + _serviceRegister = new BonjourServiceRegister(this); + _serviceRegister->registerService("_hyperiond-http._tcp", port); + } + emit stateChange(true); +} + +void WebServer::onServerStopped () { + Info(_log, "Stopped %s", _server->getServerName().toStdString().c_str()); + emit stateChange(false); +} + +void WebServer::onServerError (QString msg) +{ + Error(_log, "%s", msg.toStdString().c_str()); +} + +void WebServer::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::WEBSERVER) + { + const QJsonObject& obj = config.object(); + + _baseUrl = obj["document_root"].toString(WEBSERVER_DEFAULT_PATH); + + + if ( (_baseUrl != ":/webconfig") && !_baseUrl.trimmed().isEmpty()) + { + QFileInfo info(_baseUrl); + if (!info.exists() || !info.isDir()) + { + Error(_log, "document_root '%s' is invalid", _baseUrl.toUtf8().constData()); + _baseUrl = WEBSERVER_DEFAULT_PATH; + } + } + else + _baseUrl = WEBSERVER_DEFAULT_PATH; + + Debug(_log, "Set document root to: %s", _baseUrl.toUtf8().constData()); + _staticFileServing->setBaseUrl(_baseUrl); + + if(_port != obj["port"].toInt(WEBSERVER_DEFAULT_PORT)) + { + _port = obj["port"].toInt(WEBSERVER_DEFAULT_PORT); + stop(); + } + + // eval if the port is available, will be incremented if not + if(!_server->isListening()) + NetUtils::portAvailable(_port, _log); + + start(); + emit portChanged(_port); + } +} + +void WebServer::start() +{ + _server->start(_port); +} + +void WebServer::stop() +{ + _server->stop(); +} + +void WebServer::setSSDPDescription(const QString & desc) +{ + _staticFileServing->setSSDPDescription(desc); +} diff --git a/libsrc/webconfig/WebSocketClient.cpp b/libsrc/webserver/WebSocketClient.cpp similarity index 88% rename from libsrc/webconfig/WebSocketClient.cpp rename to libsrc/webserver/WebSocketClient.cpp index c24a796d..ebbfda52 100644 --- a/libsrc/webconfig/WebSocketClient.cpp +++ b/libsrc/webserver/WebSocketClient.cpp @@ -1,36 +1,39 @@ -#include "WebSocketClient.h" -#include "QtHttpRequest.h" -#include "QtHttpHeader.h" +#include "webserver/WebSocketClient.h" +// hyperion includes #include -#include +// JsonAPI includes +#include + +// qt includes #include #include #include +#include +#include -WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, QObject* parent) + +WebSocketClient::WebSocketClient(QByteArray socketKey, QTcpSocket* sock, QObject* parent) : QObject(parent) , _socket(sock) + , _secWebSocketKey(socketKey) , _log(Logger::getInstance("WEBSOCKET")) - , _hyperion(Hyperion::getInstance()) { // connect socket; disconnect handled from QtHttpServer connect(_socket, &QTcpSocket::readyRead , this, &WebSocketClient::handleWebSocketFrame); - // QtHttpRequest contains all headers for handshake - QByteArray secWebSocketKey = request->getHeader(QtHttpHeader::SecWebSocketKey); - const QString client = request->getClientInfo().clientAddress.toString(); + const QString client = sock->peerAddress().toString(); // Json processor - _jsonProcessor = new JsonProcessor(client, _log, this); - connect(_jsonProcessor, &JsonProcessor::callbackMessage, this, &WebSocketClient::sendMessage); + _jsonAPI = new JsonAPI(client, _log, this); + connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebSocketClient::sendMessage); Debug(_log, "New connection from %s", QSTRING_CSTR(client)); // do handshake - secWebSocketKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - QByteArray hash = QCryptographicHash::hash(secWebSocketKey, QCryptographicHash::Sha1).toBase64(); + _secWebSocketKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + QByteArray hash = QCryptographicHash::hash(_secWebSocketKey, QCryptographicHash::Sha1).toBase64(); QString data = QString("HTTP/1.1 101 Switching Protocols\r\n") @@ -110,14 +113,14 @@ void WebSocketClient::handleWebSocketFrame(void) if (_wsh.fin) { _onContinuation = false; - if (_wsh.opCode == OPCODE::TEXT) - { - _jsonProcessor->handleMessage(QString(_wsReceiveBuffer)); - } - else - { - handleBinaryMessage(_wsReceiveBuffer); - } +// if (_wsh.opCode == OPCODE::TEXT) +// { + _jsonAPI->handleMessage(QString(_wsReceiveBuffer)); +// } +// else +// { +// handleBinaryMessage(_wsReceiveBuffer); +// } _wsReceiveBuffer.clear(); } } @@ -220,10 +223,11 @@ void WebSocketClient::sendClose(int status, QString reason) _socket->close(); } +/* void WebSocketClient::handleBinaryMessage(QByteArray &data) { - uint8_t priority = data.at(0); - unsigned duration_s = data.at(1); + //uint8_t priority = data.at(0); + //unsigned duration_s = data.at(1); unsigned imgSize = data.size() - 4; unsigned width = ((data.at(2) << 8) & 0xFF00) | (data.at(3) & 0xFF); unsigned height = imgSize / width; @@ -238,8 +242,10 @@ void WebSocketClient::handleBinaryMessage(QByteArray &data) image.resize(width, height); memcpy(image.memptr(), data.data()+4, imgSize); - _hyperion->setImage(priority, image, duration_s*1000); + _hyperion->registerInput(); + _hyperion->setInputImage(priority, image, duration_s*1000); } +*/ qint64 WebSocketClient::sendMessage(QJsonObject obj) { diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt new file mode 100644 index 00000000..0305dec7 --- /dev/null +++ b/resources/CMakeLists.txt @@ -0,0 +1,22 @@ +# Global resource file to embed static data into code, available as static lib 'resources'. File names needs to be unique in the /resources directory and below. +# 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) + +# catch all files +FILE ( GLOB Hyperion_RESFILES "${CURRENT_SOURCE_DIR}/icons/*" ) + +# fill resources.qrc with RESFILES +FOREACH( f ${Hyperion_RESFILES} ) + get_filename_component(fname ${f} NAME) + 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) + +add_library(resources + ${Hyperion_RES} +) diff --git a/resources/icons/hyperion-icon-32px.png b/resources/icons/hyperion-icon-32px.png new file mode 100644 index 00000000..550ed558 Binary files /dev/null and b/resources/icons/hyperion-icon-32px.png differ diff --git a/resources/resources.qrc.in b/resources/resources.qrc.in new file mode 100644 index 00000000..e15247f3 --- /dev/null +++ b/resources/resources.qrc.in @@ -0,0 +1,5 @@ + + + ${HYPERION_RES} + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 89a91802..cac3a381 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(hyperiond) add_subdirectory(hyperion-remote) -# The following clients depend on the protobuf library +# The following binaries are just compiled if requested if (ENABLE_AMLOGIC) add_subdirectory(hyperion-aml) endif() @@ -25,4 +25,3 @@ endif() if(ENABLE_OSX) add_subdirectory(hyperion-osx) endif() - diff --git a/src/hyperion-aml/CMakeLists.txt b/src/hyperion-aml/CMakeLists.txt index 48516408..861e46bf 100644 --- a/src/hyperion-aml/CMakeLists.txt +++ b/src/hyperion-aml/CMakeLists.txt @@ -4,8 +4,8 @@ project(hyperion-aml) find_package(Qt5Widgets REQUIRED) include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/protoserver - ${PROTOBUF_INCLUDE_DIRS} + ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver + ${FLATBUFFERS_INCLUDE_DIRS} ) set(Hyperion_AML_HEADERS @@ -23,13 +23,13 @@ add_executable(${PROJECT_NAME} ) target_link_libraries(${PROJECT_NAME} - effectengine commandline - blackborder hyperion-utils - protoserver + flatbufserver + flatbuffers amlogic-grabber framebuffer-grabber + ssdp Qt5::Core Qt5::Gui Qt5::Network diff --git a/src/hyperion-aml/hyperion-aml.cpp b/src/hyperion-aml/hyperion-aml.cpp index 17077ffc..83f717bb 100644 --- a/src/hyperion-aml/hyperion-aml.cpp +++ b/src/hyperion-aml/hyperion-aml.cpp @@ -4,12 +4,15 @@ #include #include -#include +#include #include "AmlogicWrapper.h" #include "HyperionConfig.h" #include +// ssdp discover +#include + using namespace commandline; // save the image as screenshot @@ -32,13 +35,13 @@ int main(int argc, char ** argv) try { // create the option parser and initialize all parser - Parser parser("AmLogic capture application for Hyperion"); + Parser parser("AmLogic capture application for Hyperion. Will automatically search a Hyperion server if -a option isn't used. Please note that if you have more than one server running it's more or less random which one will be used."); - IntOption & argFps = parser.add ('f', "framerate", "Capture frame rate [default: %1]", "10"); + IntOption & argFps = parser.add ('f', "framerate", "Capture frame rate [default: %1]", "10", 1, 25); IntOption & argWidth = parser.add (0x0, "width", "Width of the captured image [default: %1]", "160", 160, 4096); IntOption & argHeight = parser.add (0x0, "height", "Height of the captured image [default: %1]", "160", 160, 4096); BooleanOption & argScreenshot = parser.add(0x0, "screenshot", "Take a single screenshot, save it to file and quit"); - Option & argAddress = parser.add