From b827818abe9e302a512c462e4bd64f8a17f560ca Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 19 Feb 2021 19:21:09 +0100 Subject: [PATCH 001/110] Bump development version. --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 41c6665f..34d72f27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,10 +105,10 @@ target_compile_definitions(projecteur PRIVATE # Update this information - the version numbers and the version type. # VERSION_TYPE must be either 'release' or 'develop' set_target_properties(projecteur PROPERTIES - VERSION_MAJOR 0 - VERSION_MINOR 9 + VERSION_MAJOR 1 + VERSION_MINOR 0 VERSION_PATCH 0 - VERSION_TYPE release + VERSION_TYPE develop ) add_version_info(projecteur "${CMAKE_CURRENT_SOURCE_DIR}") From deede8001bd7095a3cf516fc589b290bb964187f Mon Sep 17 00:00:00 2001 From: Jahn F Date: Fri, 26 Feb 2021 12:04:27 +0100 Subject: [PATCH 002/110] Update LinuxPkgCPackConfig.cmake.in #122 --- cmake/modules/LinuxPkgCPackConfig.cmake.in | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/modules/LinuxPkgCPackConfig.cmake.in b/cmake/modules/LinuxPkgCPackConfig.cmake.in index 4f1de07d..d0412e18 100644 --- a/cmake/modules/LinuxPkgCPackConfig.cmake.in +++ b/cmake/modules/LinuxPkgCPackConfig.cmake.in @@ -28,6 +28,7 @@ set(CPACK_RPM_PACKAGE_DESCRIPTION "@PKG_DESCRIPTION_FULL@") set(CPACK_RPM_PACKAGE_AUTOPROV 1) set(CPACK_RPM_PACKAGE_AUTOREQ 1) set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION + "/usr/lib" "@CMAKE_INSTALL_PREFIX@" "@CMAKE_INSTALL_PREFIX@/bin" "@CMAKE_INSTALL_PREFIX@/share" From 955a1cbb39c4b4b7ee3188d0c7aeaae31c9ad1dc Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 26 Feb 2021 13:08:38 +0100 Subject: [PATCH 003/110] Fix Fedora packages (again!) #122 --- CMakeLists.txt | 14 +++++++------- cmake/modules/LinuxPkgCPackConfig.cmake.in | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34d72f27..c395d00e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,13 +140,6 @@ set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS WORLD_READ WORLD_EXECUTE ) -## -- prevent additional lintian warnings 'non-standard-dir-perm' for the cmake install prefix -set(DIR_TO_INSTALL "${CMAKE_INSTALL_PREFIX}") -while(NOT "${DIR_TO_INSTALL}" STREQUAL "/" AND NOT "${DIR_TO_INSTALL}" STREQUAL "") - install(DIRECTORY DESTINATION "${DIR_TO_INSTALL}") - get_filename_component(DIR_TO_INSTALL "${DIR_TO_INSTALL}" PATH) -endwhile() - install(TARGETS projecteur DESTINATION bin) set(PROJECTEUR_INSTALL_PATH "${CMAKE_INSTALL_PREFIX}/bin/projecteur") #used in desktop file template @@ -287,6 +280,13 @@ if(PACKAGE_TARGETS) # Need to check if this would clash with packages from the debian/ubuntu repos. #configure_file("${TMPLDIR}/copyright.in" "pkg/copyright" @ONLY) #install(FILES "${OUTDIR}/pkg/copyright" DESTINATION /usr/share/doc/projecteur/) + + ## -- prevent additional lintian warnings 'non-standard-dir-perm' for the cmake install prefix + set(DIR_TO_INSTALL "${CMAKE_INSTALL_PREFIX}") + while(NOT "${DIR_TO_INSTALL}" STREQUAL "/" AND NOT "${DIR_TO_INSTALL}" STREQUAL "") + install(DIRECTORY DESTINATION "${DIR_TO_INSTALL}") + get_filename_component(DIR_TO_INSTALL "${DIR_TO_INSTALL}" PATH) + endwhile() endif() endif() diff --git a/cmake/modules/LinuxPkgCPackConfig.cmake.in b/cmake/modules/LinuxPkgCPackConfig.cmake.in index d0412e18..4f1de07d 100644 --- a/cmake/modules/LinuxPkgCPackConfig.cmake.in +++ b/cmake/modules/LinuxPkgCPackConfig.cmake.in @@ -28,7 +28,6 @@ set(CPACK_RPM_PACKAGE_DESCRIPTION "@PKG_DESCRIPTION_FULL@") set(CPACK_RPM_PACKAGE_AUTOPROV 1) set(CPACK_RPM_PACKAGE_AUTOREQ 1) set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION - "/usr/lib" "@CMAKE_INSTALL_PREFIX@" "@CMAKE_INSTALL_PREFIX@/bin" "@CMAKE_INSTALL_PREFIX@/share" From e2b055be9d0fb4c09abdd2445d669fba3d89e221 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 26 Feb 2021 23:28:54 +0100 Subject: [PATCH 004/110] Add missing virtual destructor. --- src/deviceinput.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/deviceinput.h b/src/deviceinput.h index dbdc8881..b271ad14 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -93,7 +93,7 @@ class NativeKeySequence NativeKeySequence(NativeKeySequence&&) = default; NativeKeySequence(const NativeKeySequence&) = default; - NativeKeySequence(const std::vector& qtKeys, + NativeKeySequence(const std::vector& qtKeys, std::vector&& nativeModifiers, KeyEventSequence&& kes); @@ -145,6 +145,8 @@ struct Action ToggleSpotlight = 3, }; + virtual ~Action() = default; + virtual Type type() const = 0; virtual QDataStream& save(QDataStream&) const = 0; virtual QDataStream& load(QDataStream&) = 0; From 17531ee50b3c789004c9344a6b16a947e10a49a3 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 27 Feb 2021 21:59:06 +0100 Subject: [PATCH 005/110] Add cmake option to disable man page compression #126 --- CMakeLists.txt | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c395d00e..313e58f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,15 +222,21 @@ configure_file("${TMPLDIR}/Projecteur.desktop.in" "projecteur.desktop" @ONLY) install(FILES "${OUTDIR}/projecteur.desktop" DESTINATION share/applications/) # Configure man page and gzip it. -configure_file("${TMPLDIR}/projecteur.1" "projecteur.1" @ONLY) -find_program(GZIP_EXECUTABLE gzip) -add_custom_command( - OUTPUT ${OUTDIR}/projecteur.1.gz - COMMAND ${GZIP_EXECUTABLE} -9f -n "${OUTDIR}/projecteur.1" - WORKING_DIRECTORY ${OUTDIR} -) -add_custom_target(gzip-manpage ALL DEPENDS "${OUTDIR}/projecteur.1.gz") -install(FILES "${OUTDIR}/projecteur.1.gz" DESTINATION share/man/man1/) +option(COMPRESS_MAN_PAGE "Compress the man page" ON) +configure_file("${TMPLDIR}/projecteur.1" "${OUTDIR}/projecteur.1" @ONLY) + +if(COMPRESS_MAN_PAGE) + find_program(GZIP_EXECUTABLE gzip) + add_custom_command( + OUTPUT ${OUTDIR}/projecteur.1.gz + COMMAND ${GZIP_EXECUTABLE} -9f -n "${OUTDIR}/projecteur.1" + WORKING_DIRECTORY ${OUTDIR} + ) + add_custom_target(gzip-manpage ALL DEPENDS "${OUTDIR}/projecteur.1.gz") + install(FILES "${OUTDIR}/projecteur.1.gz" DESTINATION share/man/man1/) +else() + install(FILES "${OUTDIR}/projecteur.1" DESTINATION share/man/man1/) +endif() configure_file("${TMPLDIR}/projecteur.metainfo.xml" "projecteur.metainfo.xml" @ONLY) install(FILES "${OUTDIR}/projecteur.metainfo.xml" DESTINATION share/metainfo/) @@ -270,7 +276,10 @@ if(PACKAGE_TARGETS) # PREINST_SCRIPT "${OUTDIR}/pkg/scripts/preinst" POSTINST_SCRIPT "${OUTDIR}/pkg/scripts/postinst" ) - add_dependencies(dist-package gzip-manpage projecteur) + add_dependencies(dist-package projecteur) + if(TARGET gzip-manpage) + add_dependencies(dist-package gzip-manpage) + endif() # Additional files for debian packages, adhering to some debian rules, # see https://manpages.debian.org/buster/lintian/lintian.1.en.html From 53fef5f8c98519d35172f48235d6b498efc20d2a Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 28 Feb 2021 08:18:43 +0100 Subject: [PATCH 006/110] Update cmake version in debian stretch container for correct package version generation. --- docker/Dockerfile.debian-stretch | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docker/Dockerfile.debian-stretch b/docker/Dockerfile.debian-stretch index b1e9f873..6a171116 100644 --- a/docker/Dockerfile.debian-stretch +++ b/docker/Dockerfile.debian-stretch @@ -23,3 +23,18 @@ RUN apt-get install -y --no-install-recommends \ RUN apt-get install -y --no-install-recommends \ libqt5x11extras5-dev \ libusb-1.0-0-dev + +RUN apt-get install -y --no-install-recommends \ + libqt5x11extras5-dev \ + libusb-1.0-0-dev + +RUN apt-get install -y --no-install-recommends \ + wget + +# Install newer CMake version, +# otherwise the package version in the debian package +# created by the dist-package target will not be correct +RUN wget https://github.com/Kitware/CMake/releases/download/v3.19.6/cmake-3.19.6-Linux-x86_64.sh && \ + chmod +x cmake-3.19.6-Linux-x86_64.sh && \ + ./cmake-3.19.6-Linux-x86_64.sh --skip-license --prefix=/usr && \ + rm ./cmake-3.19.6-Linux-x86_64.sh From d79eec21c04689811eb98482ec9220db5d6d14e9 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 2 Mar 2021 07:43:01 +0100 Subject: [PATCH 007/110] Remove automated upload to bintray #129 --- .github/workflows/ci-build.yml | 35 ---------------------------------- 1 file changed, 35 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index ebce6a3e..02caddbf 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -128,7 +128,6 @@ jobs: echo "upload_bin_pkg=${{ false }}" >> $GITHUB_ENV echo "upload_src_pkg=${{ false }}" >> $GITHUB_ENV echo "cloudsmith_upload_repo=projecteur-develop" >> $GITHUB_ENV - echo "bintray_upload_repo=projecteur-develop" >> $GITHUB_ENV echo "REPO_UPLOAD=${{ false }}" >> $GITHUB_ENV - name: Check for binary-pkg upload conditions @@ -146,7 +145,6 @@ jobs: - if: env.BRANCH == 'master' run: | echo "cloudsmith_upload_repo=projecteur-stable" >> $GITHUB_ENV - echo "bintray_upload_repo=projecteur-master" >> $GITHUB_ENV # =================================================================================== # ---------- Upload artifacts to cloudsmith ---------- @@ -199,36 +197,3 @@ jobs: echo Uploading for ${DISTRO} - ${PKG_TYPE}: ${CLOUDSMITH_USER}/${CLOUDSMITH_REPO}/${DISTRO} cloudsmith push ${PKG_TYPE} -W -k ${CLOUDSMITH_API_KEY} --republish \ ${CLOUDSMITH_USER}/${CLOUDSMITH_REPO}/${DISTRO} ${{ env.dist_pkg_artifact }} - - # =================================================================================== - # ---------- Upload artifacts to bintray ---------- - - name: Upload source-pkg to Bintray - if: env.upload_src_pkg == 'true' - uses: bpicode/github-action-upload-bintray@master - with: - file: ${{ env.src_pkg_artifact }} - api_user: jahnf - api_key: ${{ secrets.BINTRAY_API_KEY }} - repository_user: jahnf - repository: Projecteur - package: ${{ env.bintray_upload_repo }} - version: ${{ env.projecteur_version }} - upload_path: packages/branches/${{ env.BRANCH }}/${{ env.projecteur_version }} - calculate_metadata: false - publish: 1 - - - name: Upload binary package to Bintray - if: env.upload_bin_pkg == 'true' - uses: bpicode/github-action-upload-bintray@master - with: - file: ${{ env.dist_pkg_artifact }} - api_user: jahnf - api_key: ${{ secrets.BINTRAY_API_KEY }} - repository_user: jahnf - repository: Projecteur - package: ${{ env.bintray_upload_repo }} - version: ${{ env.projecteur_version }} - upload_path: packages/branches/${{ env.BRANCH }}/${{ env.projecteur_version }} - calculate_metadata: false - publish: 1 - From db3f602ffeaf87ea8d267c66484a7bef79d2281a Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 2 Mar 2021 08:41:38 +0100 Subject: [PATCH 008/110] Replace bintray links with cloudsmith links #129 --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 552d7eca..5d20925d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ So here it is: a Linux application for the Logitech Spotlight. [](./doc/screenshot-settings.png) [](./doc/screenshot-spot.png) [](./doc/screenshot-button-mapping.png) -[](./doc/screenshot-traymenu.png) +[](./doc/screenshot-traymenu.png) ### Planned features @@ -97,20 +97,21 @@ line option - button mapping will be disabled then.) ## Download -The latest binary packages for some Linux distributions are available for download on bintray. +The latest binary packages for some Linux distributions are available for download on cloudsmith. Currently binary packages for _Ubuntu_, _Debian_, _Fedora_, _OpenSuse_, _CentOS_ and -_Arch_ Linux are automatically built. +_Arch_ Linux are automatically built. For release version downloads see alse the project +[github releases page](https://github.com/jahnf/Projecteur/releases). -* Latest develop: [ ![Download][bintray-dev-img] ][dl-dev-bintray] -* Latest release: [ ![Download][bintray-rel-img] ][dl-rel-bintray] +* [**Latest release:** ![cloudsmith-rel-badge]][cloudsmith-rel-latest] +* [Latest development version: ![cloudsmith-dev-badge]][cloudsmith-dev-latest] -See also the [list of Linux repositories](./doc/LinuxRepositories.md) where _Projecteur_ +See also the **[list of Linux repositories](./doc/LinuxRepositories.md)** where _Projecteur_ is available. -[dl-dev-bintray]: https://bintray.com/jahnf/Projecteur/projecteur-develop/_latestVersion#files -[dl-rel-bintray]: https://bintray.com/jahnf/Projecteur/projecteur-master/_latestVersion#files -[bintray-dev-img]: https://api.bintray.com/packages/jahnf/Projecteur/projecteur-develop/images/download.svg -[bintray-rel-img]: https://api.bintray.com/packages/jahnf/Projecteur/projecteur-master/images/download.svg +[cloudsmith-rel-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-stable/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPebvngKb3w0EsZUr_IHIIzlfYCipDOGxcJdzMRGI3BLdVsLf62Na7Cg6q11ps7yNgv3kR9KXyxJyjFFbPs2eTAGzvL-UXTonyqSY5D1fwva_o_g%3D +[cloudsmith-rel-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-stable/packages/?q=format%3Araw+tag%3Alatest +[cloudsmith-dev-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-develop/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPd_g3txb3xWrIHsaUrhBB7hOamTwfPVpR7xGUELEaQ0pGnxFnXO1cqTPAMDcTjRsHM2zAjx00OXU_5ARSQDofAUe6lIqKrKNykiMhVT_jlZAy-4%3D +[cloudsmith-dev-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-develop/packages/?q=format%3Araw+tag%3Alatest ## Building From 5e1ed6c28f9cfc32cc394f272b600416cfc2e290 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 2 Mar 2021 08:43:20 +0100 Subject: [PATCH 009/110] Update changelog with missing v0.9.1 info. --- doc/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 48d1cc0c..0f1ce765 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.9.1 + +### Changes/Updates: + +- Fixes for automatically generated RPM Packages (especially Fedora) +- Fixes for version numbers in generated packages (DEB and RPM) + ## v0.9 ### Changes/Updates: From f45ef80b43fc279880227fb2b69ce64a45a74e0e Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 6 Mar 2021 08:22:36 +0100 Subject: [PATCH 010/110] Add additional ci binary upload. --- .github/workflows/ci-build.yml | 65 ++++++++++++++++++++++++++++++++-- README.md | 2 ++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 02caddbf..e0a22a54 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -46,7 +46,7 @@ jobs: - run: | export LOCAL_BIN=~/.local/bin echo "${LOCAL_BIN}" >> $GITHUB_PATH - + # =================================================================================== # ---------- Checkout and build inside docker container ---------- - uses: actions/checkout@v1 @@ -106,15 +106,27 @@ jobs: dist_pkg_artifact=`ls -1 dist-pkg/* | head -n 1` echo "dist_pkg_artifact=${dist_pkg_artifact}" >> $GITHUB_ENV + - if: startsWith(matrix.docker_tag, 'archlinux') + run: echo "${{ env.BRANCH }}" >> version-branch + # =================================================================================== # ---------- Upload artifacts to github ---------- - name: Upload source-pkg artifact to github if: startsWith(matrix.docker_tag, 'archlinux') - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: name: source-package path: ${{ env.src_pkg_artifact }} + - name: Upload version-info to github + if: startsWith(matrix.docker_tag, 'archlinux') + uses: actions/upload-artifact@v2 + with: + name: version-info + path: | + ./version-string + ./version-branch + - name: Upload binary package artifact to github uses: actions/upload-artifact@v2 with: @@ -197,3 +209,52 @@ jobs: echo Uploading for ${DISTRO} - ${PKG_TYPE}: ${CLOUDSMITH_USER}/${CLOUDSMITH_REPO}/${DISTRO} cloudsmith push ${PKG_TYPE} -W -k ${CLOUDSMITH_API_KEY} --republish \ ${CLOUDSMITH_USER}/${CLOUDSMITH_REPO}/${DISTRO} ${{ env.dist_pkg_artifact }} + + # ===================================================================================== + # ---------- Upload artifacts to projecteur server ------------ + projecteur-bin-upload: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Get version-info + uses: actions/download-artifact@v2 + with: + name: version-info + + - name: Extract version info + run: | + BRANCH=`cat version-branch` + echo "BRANCH=${BRANCH}" >> $GITHUB_ENV + VERSION=`cat version-string` + echo "VERSION=${VERSION}" >> $GITHUB_ENV + DO_UPLOAD=$(( [ "master" = "$BRANCH" ] || [ "develop" = "$BRANCH" ] ) && echo true || echo false) + echo "DO_UPLOAD=${DO_UPLOAD}" >> $GITHUB_ENV + + - uses: actions/download-artifact@v2 + if: env.DO_UPLOAD == 'true' + with: + path: artifacts + + - name: Create upload directory + if: env.DO_UPLOAD == 'true' + run: | + BRANCHDIR=${{ env.BRANCH }} + [ "master" = "$BRANCHDIR" ] && BRANCHDIR=stable + VERSION=${{ env.VERSION }} + mkdir -p upload/$BRANCHDIR/$VERSION + find ./artifacts -iname "projecteur*" -exec mv -t upload/$BRANCHDIR/$VERSION {} + + BRANCH_FILENAME=${BRANCHDIR/\//_}-latest.json + echo '{ "version": "${{ env.VERSION}}" }' >> upload/$BRANCH_FILENAME + find . -iname "projecteur*" + + - name: 📂 Upload files + if: env.DO_UPLOAD == 'true' + run: | + cd upload && sudo apt-get install lftp --no-install-recommends + lftp ${{ secrets.PROJECTEUR_UPLOAD_HOSTNAME }} \ + -u "${{ secrets.PROJECTEUR_UPLOAD_USER }},${{ secrets.PROJECTEUR_UPLOAD_TOKEN }}" \ + -e "set ftp:ssl-force true; set ssl:verify-certificate true; mirror \ + --reverse --upload-older --dereference -x ^\.git/$ ./ ./; quit" + + diff --git a/README.md b/README.md index 5d20925d..94f69dd3 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ is available. [cloudsmith-rel-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-stable/packages/?q=format%3Araw+tag%3Alatest [cloudsmith-dev-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-develop/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPd_g3txb3xWrIHsaUrhBB7hOamTwfPVpR7xGUELEaQ0pGnxFnXO1cqTPAMDcTjRsHM2zAjx00OXU_5ARSQDofAUe6lIqKrKNykiMhVT_jlZAy-4%3D [cloudsmith-dev-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-develop/packages/?q=format%3Araw+tag%3Alatest +[projecteur-rel-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fstable-latest.json +[projecteur-dev-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fdevelop-latest.json ## Building From eae6ab9bcaa5541ef166e03c78cb093b7d6fbcb5 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 6 Mar 2021 10:15:15 +0100 Subject: [PATCH 011/110] Update 'latest' symlinks in ci build. --- .github/workflows/ci-build.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index e0a22a54..79fa1c61 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -46,7 +46,7 @@ jobs: - run: | export LOCAL_BIN=~/.local/bin echo "${LOCAL_BIN}" >> $GITHUB_PATH - + # =================================================================================== # ---------- Checkout and build inside docker container ---------- - uses: actions/checkout@v1 @@ -209,19 +209,19 @@ jobs: echo Uploading for ${DISTRO} - ${PKG_TYPE}: ${CLOUDSMITH_USER}/${CLOUDSMITH_REPO}/${DISTRO} cloudsmith push ${PKG_TYPE} -W -k ${CLOUDSMITH_API_KEY} --republish \ ${CLOUDSMITH_USER}/${CLOUDSMITH_REPO}/${DISTRO} ${{ env.dist_pkg_artifact }} - + # ===================================================================================== # ---------- Upload artifacts to projecteur server ------------ projecteur-bin-upload: needs: build runs-on: ubuntu-latest - + steps: - name: Get version-info uses: actions/download-artifact@v2 with: name: version-info - + - name: Extract version info run: | BRANCH=`cat version-branch` @@ -230,7 +230,7 @@ jobs: echo "VERSION=${VERSION}" >> $GITHUB_ENV DO_UPLOAD=$(( [ "master" = "$BRANCH" ] || [ "develop" = "$BRANCH" ] ) && echo true || echo false) echo "DO_UPLOAD=${DO_UPLOAD}" >> $GITHUB_ENV - + - uses: actions/download-artifact@v2 if: env.DO_UPLOAD == 'true' with: @@ -244,11 +244,13 @@ jobs: VERSION=${{ env.VERSION }} mkdir -p upload/$BRANCHDIR/$VERSION find ./artifacts -iname "projecteur*" -exec mv -t upload/$BRANCHDIR/$VERSION {} + - BRANCH_FILENAME=${BRANCHDIR/\//_}-latest.json + BRANCHNAME=${BRANCHDIR/\//_} + BRANCH_FILENAME=${BRANCHNAME}-latest.json echo '{ "version": "${{ env.VERSION}}" }' >> upload/$BRANCH_FILENAME + echo "BRANCHNAME=${BRANCHNAME}" >> $GITHUB_ENV find . -iname "projecteur*" - - name: 📂 Upload files + - name: 📂 Upload files if: env.DO_UPLOAD == 'true' run: | cd upload && sudo apt-get install lftp --no-install-recommends @@ -257,4 +259,7 @@ jobs: -e "set ftp:ssl-force true; set ssl:verify-certificate true; mirror \ --reverse --upload-older --dereference -x ^\.git/$ ./ ./; quit" - + - name: Update latest symlink + run: | + curl --fail -i -X POST -F "token=${{ secrets.PROJECTEUR_UPDATE_TOKEN }}" \ + ${{ secrets.PROJECTEUR_UPDATE_URL }}?branch=${{ env.BRANCHNAME }} From 03926f7698a3cc3b07647cadfb1c1388c7886df7 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Sat, 6 Mar 2021 10:21:47 +0100 Subject: [PATCH 012/110] Update ci-build.yml --- .github/workflows/ci-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 79fa1c61..8434b38f 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -260,6 +260,7 @@ jobs: --reverse --upload-older --dereference -x ^\.git/$ ./ ./; quit" - name: Update latest symlink + if: env.DO_UPLOAD == 'true' run: | curl --fail -i -X POST -F "token=${{ secrets.PROJECTEUR_UPDATE_TOKEN }}" \ ${{ secrets.PROJECTEUR_UPDATE_URL }}?branch=${{ env.BRANCHNAME }} From 45cc9bc167586f5d4d57640936e83bcfc82053d6 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 6 Mar 2021 10:29:55 +0100 Subject: [PATCH 013/110] Add Ubuntu 21.04 CI build. --- .github/workflows/ci-build.yml | 4 +++- docker/Dockerfile.ubuntu-19.04 | 26 -------------------------- docker/Dockerfile.ubuntu-19.10 | 26 -------------------------- docker/Dockerfile.ubuntu-21.04 | 21 +++++++++++++++++++++ 4 files changed, 24 insertions(+), 53 deletions(-) delete mode 100644 docker/Dockerfile.ubuntu-19.04 delete mode 100644 docker/Dockerfile.ubuntu-19.10 create mode 100644 docker/Dockerfile.ubuntu-21.04 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 02caddbf..b5e88628 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -24,6 +24,7 @@ jobs: - ubuntu-18.04 - ubuntu-20.04 - ubuntu-20.10 + - ubuntu-21.04 - opensuse-15.0 - opensuse-15.1 - opensuse-15.2 @@ -175,7 +176,8 @@ jobs: export PKG_TYPE="${filename##*.}" declare -A distromap=( ["debian-stretch"]="debian/stretch" ["debian-buster"]="debian/buster" \ ["debian-bullseye"]="debian/bullseye" ["ubuntu-18.04"]="ubuntu/bionic" \ - ["ubuntu-20.04"]="ubuntu/focal" ["opensuse-15.1"]="opensuse/15.1" \ + ["ubuntu-20.04"]="ubuntu/focal" ["ubuntu-21.04"]="ubuntu/hirsute" \ + ["opensuse-15.1"]="opensuse/15.1" \ ["opensuse-15.2"]="opensuse/15.2" ["centos-8"]="el/8" \ ["fedora-30"]="fedora/30" ["fedora-31"]="fedora/31" \ ["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" ) diff --git a/docker/Dockerfile.ubuntu-19.04 b/docker/Dockerfile.ubuntu-19.04 deleted file mode 100644 index 1a033fc0..00000000 --- a/docker/Dockerfile.ubuntu-19.04 +++ /dev/null @@ -1,26 +0,0 @@ -# Container for building the Projecteur package -# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags - -FROM ubuntu:19.04 - -RUN apt-get update -RUN apt-get install -y --no-install-recommends \ - ca-certificates - -RUN apt-get install -y --no-install-recommends \ - g++ \ - make \ - cmake \ - udev \ - git \ - pkg-config - -RUN apt-get install -y --no-install-recommends \ - qtdeclarative5-dev \ - qttools5-dev-tools \ - qt5-default - -RUN apt-get install -y --no-install-recommends \ - libqt5x11extras5-dev \ - libusb-1.0-0-dev - diff --git a/docker/Dockerfile.ubuntu-19.10 b/docker/Dockerfile.ubuntu-19.10 deleted file mode 100644 index 5f0d4bbf..00000000 --- a/docker/Dockerfile.ubuntu-19.10 +++ /dev/null @@ -1,26 +0,0 @@ -# Container for building the Projecteur package -# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags - -FROM ubuntu:19.10 - -RUN apt-get update -RUN apt-get install -y --no-install-recommends \ - ca-certificates - -RUN apt-get install -y --no-install-recommends \ - g++ \ - make \ - cmake \ - udev \ - git \ - pkg-config - -RUN apt-get install -y --no-install-recommends \ - qtdeclarative5-dev \ - qttools5-dev-tools \ - qt5-default - -RUN apt-get install -y --no-install-recommends \ - libqt5x11extras5-dev \ - libusb-1.0-0-dev - diff --git a/docker/Dockerfile.ubuntu-21.04 b/docker/Dockerfile.ubuntu-21.04 new file mode 100644 index 00000000..86f9871b --- /dev/null +++ b/docker/Dockerfile.ubuntu-21.04 @@ -0,0 +1,21 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM ubuntu:21.04 + +RUN apt-get update && mkdir /build +RUN DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + g++ \ + make \ + cmake \ + udev \ + git \ + pkg-config \ + qtdeclarative5-dev \ + qttools5-dev-tools \ + qttools5-dev \ + libqt5x11extras5-dev \ + libusb-1.0-0-dev \ + && rm -rf /var/lib/apt/lists/* From fed56415f90166d23be74b5bd7f71b3c2c2e913c Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 6 Mar 2021 11:18:51 +0100 Subject: [PATCH 014/110] Add secondary download locations to README. --- .github/workflows/ci-build.yml | 2 ++ README.md | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 8434b38f..103e5f34 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -249,6 +249,8 @@ jobs: echo '{ "version": "${{ env.VERSION}}" }' >> upload/$BRANCH_FILENAME echo "BRANCHNAME=${BRANCHNAME}" >> $GITHUB_ENV find . -iname "projecteur*" + cd upload/$BRANCHDIR/$VERSION + sha1sum * > sha1sums.txt - name: 📂 Upload files if: env.DO_UPLOAD == 'true' diff --git a/README.md b/README.md index 94f69dd3..b7ceadf5 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,12 @@ Currently binary packages for _Ubuntu_, _Debian_, _Fedora_, _OpenSuse_, _CentOS_ _Arch_ Linux are automatically built. For release version downloads see alse the project [github releases page](https://github.com/jahnf/Projecteur/releases). -* [**Latest release:** ![cloudsmith-rel-badge]][cloudsmith-rel-latest] -* [Latest development version: ![cloudsmith-dev-badge]][cloudsmith-dev-latest] +* **Latest release:** + * on cloudsmith: [![cloudsmith-rel-badge]][cloudsmith-rel-latest] + * on secondery server: [![projecteur-rel-badge]][projecteur-rel-dl] +* Latest development version: + * on cloudsmith: [![cloudsmith-dev-badge]][cloudsmith-dev-latest] + * on secondary server: [![projecteur-dev-badge]][projecteur-dev-dl] See also the **[list of Linux repositories](./doc/LinuxRepositories.md)** where _Projecteur_ is available. @@ -114,6 +118,8 @@ is available. [cloudsmith-dev-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-develop/packages/?q=format%3Araw+tag%3Alatest [projecteur-rel-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fstable-latest.json [projecteur-dev-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fdevelop-latest.json +[projecteur-dev-dl]: https://projecteur.de/downloads/develop/latest +[projecteur-rel-dl]: https://projecteur.de/downloads/stable/latest ## Building From b3debbddebe7a05e5eaf1eec3806489dadb3d968 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Sat, 6 Mar 2021 11:45:17 +0100 Subject: [PATCH 015/110] Add workaround for out of date cloudsmith badges. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b7ceadf5..d9b054c1 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,9 @@ _Arch_ Linux are automatically built. For release version downloads see alse the See also the **[list of Linux repositories](./doc/LinuxRepositories.md)** where _Projecteur_ is available. -[cloudsmith-rel-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-stable/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPebvngKb3w0EsZUr_IHIIzlfYCipDOGxcJdzMRGI3BLdVsLf62Na7Cg6q11ps7yNgv3kR9KXyxJyjFFbPs2eTAGzvL-UXTonyqSY5D1fwva_o_g%3D +[cloudsmith-rel-badge]: https://img.shields.io/badge/dynamic/json?color=blue&labelColor=12577e&logo=cloudsmith&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fstable-latest.json [cloudsmith-rel-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-stable/packages/?q=format%3Araw+tag%3Alatest -[cloudsmith-dev-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-develop/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPd_g3txb3xWrIHsaUrhBB7hOamTwfPVpR7xGUELEaQ0pGnxFnXO1cqTPAMDcTjRsHM2zAjx00OXU_5ARSQDofAUe6lIqKrKNykiMhVT_jlZAy-4%3D +[cloudsmith-dev-badge]: https://img.shields.io/badge/dynamic/json?color=blue&labelColor=12577e&logo=cloudsmith&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fdevelop-latest.json [cloudsmith-dev-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-develop/packages/?q=format%3Araw+tag%3Alatest [projecteur-rel-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fstable-latest.json [projecteur-dev-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fdevelop-latest.json From dfb52bb0b86189ac5cc9383011e0028d2271ccb4 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Wed, 10 Mar 2021 10:59:25 +0100 Subject: [PATCH 016/110] Fix typo in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d9b054c1..fc71ab4f 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ line option - button mapping will be disabled then.) The latest binary packages for some Linux distributions are available for download on cloudsmith. Currently binary packages for _Ubuntu_, _Debian_, _Fedora_, _OpenSuse_, _CentOS_ and -_Arch_ Linux are automatically built. For release version downloads see alse the project -[github releases page](https://github.com/jahnf/Projecteur/releases). +_Arch_ Linux are automatically built. For release version downloads you can also visit +the project's [github releases page](https://github.com/jahnf/Projecteur/releases). * **Latest release:** * on cloudsmith: [![cloudsmith-rel-badge]][cloudsmith-rel-latest] From c397f54905fb56659563f9284ac3016095c34061 Mon Sep 17 00:00:00 2001 From: Jahn Date: Mon, 22 Mar 2021 19:14:48 +0100 Subject: [PATCH 017/110] Remove ubuntu 18.10 docker file. --- README.md | 11 ++++++----- docker/Dockerfile.ubuntu-18.10 | 26 -------------------------- 2 files changed, 6 insertions(+), 31 deletions(-) delete mode 100644 docker/Dockerfile.ubuntu-18.10 diff --git a/README.md b/README.md index 5d20925d..48fa8fb7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Projecteur -develop: [ ![Build Status develop][gh-badge-dev] ][gh-link-dev] -master: [ ![Build Status master][gh-badge-rel] ][gh-link-rel] +develop: [![Build Status develop][gh-badge-dev]][gh-link-dev] +master: [![Build Status master][gh-badge-rel]][gh-link-rel] Linux/X11 application for the Logitech Spotlight device (and similar devices). \ See **[Download](#download)** section for binary packages. @@ -123,7 +123,7 @@ is available. ### Build Example -``` +```sh git clone https://github.com/jahnf/Projecteur cd Projecteur mkdir build && cd build @@ -171,7 +171,7 @@ see the [Troubleshooting](#missing-system-tray) section. Additional to the standard `--help` and `--version` options, there is an option to send commands to a running instance of _Projecteur_ and the ability to set properties. -``` +```txt Usage: projecteur [OPTION]... @@ -203,6 +203,7 @@ _Projecteur_ allows you to set almost all aspects of the spotlight via the comma for a running instance. Example: + ```bash # Set showing the border to true projecteur -c border=true @@ -298,7 +299,7 @@ While not developed with Wayland in mind, users reported _Projecteur_ works with Wayland. If you experience problems, you can try to set the `QT_QPA_PLATFORM` environment variable to `wayland`, example: -``` +```bash user@ubuntu1904:~/Projecteur/build$ QT_QPA_PLATFORM=wayland ./projecteur Using Wayland-EGL ``` diff --git a/docker/Dockerfile.ubuntu-18.10 b/docker/Dockerfile.ubuntu-18.10 deleted file mode 100644 index ea6660fd..00000000 --- a/docker/Dockerfile.ubuntu-18.10 +++ /dev/null @@ -1,26 +0,0 @@ -# Container for building the Projecteur package -# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags - -FROM ubuntu:18.10 - -RUN apt-get update -RUN apt-get install -y --no-install-recommends \ - ca-certificates - -RUN apt-get install -y --no-install-recommends \ - g++ \ - make \ - cmake \ - udev \ - git \ - pkg-config - -RUN apt-get install -y --no-install-recommends \ - qtdeclarative5-dev \ - qttools5-dev-tools \ - qt5-default - -RUN apt-get install -y --no-install-recommends \ - libqt5x11extras5-dev \ - libusb-1.0-0-dev - From 23a5773330d902f3ffe04e0518a44b76e3c58e3f Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Wed, 14 Jul 2021 15:12:48 +0530 Subject: [PATCH 018/110] Read and write notifier for HID subdevice This commit introduces separate read and write QSocketNotifier for subdevices. Write notifier is required for sending data to HID subdevice. Read notifier for HID subdevice will help in reading data from device like battery percentage and configuring the spotlight device like configuring it to send long press events for Next and previous button (See jahnf#71). This commit also fixes jahnf#133 as read notifier for HID subdevice do not send activated signal continuosly after a write operation. Rather the on activated signal, the device is read (However it currently does not do anything with the read value). --- src/device-vibration.cc | 7 +-- src/device.cc | 97 +++++++++++++++++++++++++++++++++-------- src/device.h | 11 ++++- src/spotlight.cc | 48 ++++++++++++++++++-- src/spotlight.h | 2 + 5 files changed, 139 insertions(+), 26 deletions(-) diff --git a/src/device-vibration.cc b/src/device-vibration.cc index a5e427ed..895d5a59 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -407,7 +407,9 @@ void VibrationSettingsWidget::setIntensity(uint8_t intensity) // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc) { - m_subDeviceConnection = sdc; + if (sdc->type() == ConnectionType::Hidraw && + sdc->mode() == ConnectionMode::ReadWrite) + m_subDeviceConnection = sdc; } // ------------------------------------------------------------------------------------------------- @@ -427,8 +429,7 @@ void VibrationSettingsWidget::sendVibrateCommand() const uint8_t vint = m_sbIntensity->value(); const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1a, vlen, 0xe8, vint}; - const auto notifier = m_subDeviceConnection->socketNotifier(); - const auto res = ::write(notifier->socket(), &vibrateCmd[0], sizeof(vibrateCmd)); + const auto res = m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); if (res != sizeof(vibrateCmd)) { logWarn(device) << "Could not write vibrate command to device socket."; } diff --git a/src/device.cc b/src/device.cc index 92a71028..eee9b78e 100644 --- a/src/device.cc +++ b/src/device.cc @@ -9,10 +9,10 @@ #include #include -#include #include LOGGING_CATEGORY(device, "device") +LOGGING_CATEGORY(hid, "HID") namespace { // ----------------------------------------------------------------------------------------------- @@ -72,17 +72,33 @@ SubDeviceConnection::~SubDeviceConnection() = default; // ------------------------------------------------------------------------------------------------- bool SubDeviceConnection::isConnected() const { - return m_notifier && m_notifier->isEnabled(); + if (type() == ConnectionType::Event) + return (m_readNotifier && m_readNotifier->isEnabled()); + if (type() == ConnectionType::Hidraw) + return (m_readNotifier && m_readNotifier->isEnabled()) && (m_writeNotifier); + return false; } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::disconnect() { - m_notifier.reset(); + m_readNotifier.reset(); + m_writeNotifier.reset(); } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::disable() { - if (m_notifier) m_notifier->setEnabled(false); + if (m_readNotifier) m_readNotifier->setEnabled(false); + if (m_writeNotifier) m_writeNotifier->setEnabled(false); +} + +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::disableWrite() { + if (m_writeNotifier) m_writeNotifier->setEnabled(false); +} + +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::enableWrite() { + if (m_writeNotifier) m_writeNotifier->setEnabled(true); } // ------------------------------------------------------------------------------------------------- @@ -91,8 +107,13 @@ const std::shared_ptr& SubDeviceConnection::inputMapper() const { } // ------------------------------------------------------------------------------------------------- -QSocketNotifier* SubDeviceConnection::socketNotifier() { - return m_notifier.get(); +QSocketNotifier* SubDeviceConnection::socketReadNotifier() { + return m_readNotifier.get(); +} + +// ------------------------------------------------------------------------------------------------- +QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { + return m_writeNotifier.get(); } // ------------------------------------------------------------------------------------------------- @@ -105,6 +126,11 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: { const int evfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDONLY, 0); + if (evfd == -1) { + logWarn(device) << tr("Cannot open event device '%1' for read.").arg(sd.deviceFile); + return std::shared_ptr(); + } + struct input_id id{}; ioctl(evfd, EVIOCGID, &id); // get the event sub-device id @@ -163,8 +189,8 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } // Create socket notifier - connection->m_notifier = std::make_unique(evfd, QSocketNotifier::Read); - QSocketNotifier* const notifier = connection->m_notifier.get(); + connection->m_readNotifier = std::make_unique(evfd, QSocketNotifier::Read); + QSocketNotifier* const notifier = connection->m_readNotifier.get(); // Auto clean up and close descriptor on destruction of notifier connect(notifier, &QSocketNotifier::destroyed, [grabbed = connection->m_details.grabbed, notifier]() { if (grabbed) { @@ -190,28 +216,28 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca const int devfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDWR|O_NONBLOCK , 0); if (devfd == -1) { - logWarn(device) << tr("Could not open hidraw device '%1' for read/write.").arg(sd.deviceFile); + logWarn(device) << tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile); return std::shared_ptr(); } int descriptorSize = 0; // Get Report Descriptor Size if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0) { - logWarn(device) << tr("Could retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); + logWarn(device) << tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); return std::shared_ptr(); } struct hidraw_report_descriptor reportDescriptor{}; reportDescriptor.size = descriptorSize; if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0) { - logWarn(device) << tr("Could retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); + logWarn(device) << tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile); return std::shared_ptr(); } struct hidraw_devinfo devinfo{}; // get the hidraw sub-device id info if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0) { - logWarn(device) << tr("Could get info from hidraw device '%1'.").arg(sd.deviceFile); + logWarn(device) << tr("Cannot get info from hidraw device '%1'.").arg(sd.deviceFile); return std::shared_ptr(); }; @@ -238,14 +264,51 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca connection->m_details.deviceFlags |= DeviceFlag::Vibrate; } - // Create socket notifier - connection->m_notifier = std::make_unique(devfd, QSocketNotifier::Read); - QSocketNotifier* const notifier = connection->m_notifier.get(); + // Create read and write socket notifiers + connection->m_readNotifier = std::make_unique(devfd, QSocketNotifier::Read); + QSocketNotifier* const readNotifier = connection->m_readNotifier.get(); // Auto clean up and close descriptor on destruction of notifier - connect(notifier, &QSocketNotifier::destroyed, [notifier]() { - ::close(static_cast(notifier->socket())); + connect(readNotifier, &QSocketNotifier::destroyed, [readNotifier]() { + ::close(static_cast(readNotifier->socket())); + }); + + connection->m_writeNotifier = std::make_unique(devfd, QSocketNotifier::Write); + QSocketNotifier* const writeNotifier = connection->m_writeNotifier.get(); + // Auto clean up and close descriptor on destruction of notifier + connect(writeNotifier, &QSocketNotifier::destroyed, [writeNotifier]() { + ::close(static_cast(writeNotifier->socket())); }); connection->m_details.phys = sd.phys; + connection->disableWrite(); // disable write notifier return connection; } + +// ------------------------------------------------------------------------------------------------- +ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg) +{ + ssize_t res = -1; + bool isValidMsg = (hidppMsg.length() == 7 && hidppMsg.at(0) == 0x10); // HID++ short message + isValidMsg = isValidMsg || (hidppMsg.length() == 20 && hidppMsg.at(0) == 0x11); // HID++ long message + + if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite + && m_writeNotifier && isValidMsg) + { + enableWrite(); + const auto notifier = socketWriteNotifier(); + res = ::write(notifier->socket(), hidppMsg.data(), hidppMsg.length()); + logDebug(hid) << "Write" << hidppMsg.toHex(':') << "to" << path(); + disableWrite(); + } + + return res; +} + + +// ------------------------------------------------------------------------------------------------- +ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen) +{ + const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen); + + return sendData(hidppMsgArr); +} diff --git a/src/device.h b/src/device.h index 36f29504..b4a90c89 100644 --- a/src/device.h +++ b/src/device.h @@ -127,6 +127,11 @@ class SubDeviceConnection : public QObject bool isConnected() const; void disconnect(); // destroys socket notifier and close file handle void disable(); // disable receiving/sending data + void disableWrite(); // disable sending data + void enableWrite(); // enable sending data + + ssize_t sendData(const QByteArray& hidppMsg); // Send HID++ Message to HIDraw connection + ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen); // Send HID++ Message to HIDraw connection auto type() const { return m_details.type; }; auto mode() const { return m_details.mode; }; @@ -136,14 +141,16 @@ class SubDeviceConnection : public QObject const auto& path() const { return m_details.devicePath; }; const std::shared_ptr& inputMapper() const; - QSocketNotifier* socketNotifier(); + QSocketNotifier* socketReadNotifier(); // Read notifier for Hidraw and Event connections for receiving data from device + QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device protected: SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode); SubDeviceConnectionDetails m_details; std::shared_ptr m_inputMapper; // shared input mapper from parent device. - std::unique_ptr m_notifier; + std::unique_ptr m_readNotifier; + std::unique_ptr m_writeNotifier; // only useful for Hidraw connections }; // ------------------------------------------------------------------------------------------------- diff --git a/src/spotlight.cc b/src/spotlight.cc index 6f68fe22..e2d300a4 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -13,10 +13,10 @@ #include #include #include -#include #include DECLARE_LOGGING_CATEGORY(device) +DECLARE_LOGGING_CATEGORY(hid) namespace { const auto hexId = logging::hexId; @@ -132,7 +132,8 @@ int Spotlight::connectDevices() auto devCon = SubEventConnection::create(scanSubDevice, *dc); if (addInputEventHandler(devCon)) return devCon; } else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { - return SubHidrawConnection::create(scanSubDevice, *dc); + auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); + if(addHIDInputHandler(hidCon)) return hidCon; } return std::shared_ptr(); }(); @@ -288,6 +289,29 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) } // end while loop } +// ------------------------------------------------------------------------------------------------- +void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) +{ + QByteArray readVal(20, 0); + if (::read(fd, static_cast(readVal.data()), readVal.length()) < 0) + { + if (errno != EAGAIN) + { + const bool anyConnectedBefore = anySpotlightDeviceConnected(); + connection.disable(); + QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ + removeDeviceConnection(devicePath); + if (!anySpotlightDeviceConnected() && anyConnectedBefore) { + emit anySpotlightDeviceConnectedChanged(false); + } + }); + } + return; + } + logDebug(hid) << "Received" << readVal.toHex(':') << "from" << connection.path(); + // TODO: Process Logitech HIDPP message +} + // ------------------------------------------------------------------------------------------------- bool Spotlight::addInputEventHandler(std::shared_ptr connection) { @@ -295,8 +319,8 @@ bool Spotlight::addInputEventHandler(std::shared_ptr connect return false; } - QSocketNotifier* const notifier = connection->socketNotifier(); - connect(notifier, &QSocketNotifier::activated, this, + QSocketNotifier* const readNotifier = connection->socketReadNotifier(); + connect(readNotifier, &QSocketNotifier::activated, this, [this, connection=std::move(connection)](int fd) { onEventDataAvailable(fd, *connection.get()); }); @@ -304,6 +328,22 @@ bool Spotlight::addInputEventHandler(std::shared_ptr connect return true; } +// ------------------------------------------------------------------------------------------------- +bool Spotlight::addHIDInputHandler(std::shared_ptr connection) +{ + if (!connection || connection->type() != ConnectionType::Hidraw || !connection->isConnected()) { + return false; + } + + QSocketNotifier* const readNotifier = connection->socketReadNotifier(); + connect(readNotifier, &QSocketNotifier::activated, this, + [this, connection=std::move(connection)](int fd) { + onHIDDataAvailable(fd, *connection.get()); + }); + + return true; +} + // ------------------------------------------------------------------------------------------------- bool Spotlight::setupDevEventInotify() { diff --git a/src/spotlight.h b/src/spotlight.h index 2174e11b..235d1fe2 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -54,11 +54,13 @@ class Spotlight : public QObject ConnectionResult connectSpotlightDevice(const QString& devicePath, bool verbose = false); bool addInputEventHandler(std::shared_ptr connection); + bool addHIDInputHandler(std::shared_ptr connection); bool setupDevEventInotify(); int connectDevices(); void removeDeviceConnection(const QString& devicePath); void onEventDataAvailable(int fd, SubEventConnection& connection); + void onHIDDataAvailable(int fd, SubHidrawConnection& connection); const Options m_options; std::map> m_deviceConnections; From caa820272a3eae24f8da2759607d83db9b8b9bc7 Mon Sep 17 00:00:00 2001 From: Jahn Date: Wed, 14 Jul 2021 12:13:17 +0200 Subject: [PATCH 019/110] Fix build for older Qt versions. #133 --- src/device.cc | 2 +- src/spotlight.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/device.cc b/src/device.cc index eee9b78e..60188cdb 100644 --- a/src/device.cc +++ b/src/device.cc @@ -297,7 +297,7 @@ ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg) enableWrite(); const auto notifier = socketWriteNotifier(); res = ::write(notifier->socket(), hidppMsg.data(), hidppMsg.length()); - logDebug(hid) << "Write" << hidppMsg.toHex(':') << "to" << path(); + logDebug(hid) << "Write" << hidppMsg.toHex() << "to" << path(); disableWrite(); } diff --git a/src/spotlight.cc b/src/spotlight.cc index e2d300a4..23d75531 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -308,7 +308,7 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } return; } - logDebug(hid) << "Received" << readVal.toHex(':') << "from" << connection.path(); + logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); // TODO: Process Logitech HIDPP message } From afda592f4119af14a5b89baf050dbc068f44da10 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Fri, 16 Jul 2021 02:00:25 +0530 Subject: [PATCH 020/110] Fix Seg fault on device disconnect --- src/device-vibration.cc | 4 +--- src/device.cc | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/device-vibration.cc b/src/device-vibration.cc index 895d5a59..d1120460 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -407,9 +407,7 @@ void VibrationSettingsWidget::setIntensity(uint8_t intensity) // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc) { - if (sdc->type() == ConnectionType::Hidraw && - sdc->mode() == ConnectionMode::ReadWrite) - m_subDeviceConnection = sdc; + m_subDeviceConnection = sdc; } // ------------------------------------------------------------------------------------------------- diff --git a/src/device.cc b/src/device.cc index 60188cdb..2e91037f 100644 --- a/src/device.cc +++ b/src/device.cc @@ -309,6 +309,5 @@ ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg) ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen) { const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen); - return sendData(hidppMsgArr); } From 5c4faafe5fafb383e2b31e5f496b1bb2b85683df Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Fri, 16 Jul 2021 20:11:41 +0530 Subject: [PATCH 021/110] Enable Hidraw interface on spotlight bluetooth connection This commit enables hidraw interface on logitech spotlight connected via bluetooth. Three major changes are 1. DeviceId (in device.h) now include the information about the bus on which spotlight is connected (USB/Bluetooth). 2. sendData function (device.cc) for hidraw on bluetooth modifies the data before sending it. The bluetooth hid need the data in 20 byte long packages; smaller packet of 7 byte length is not allowed on bluetooth connection. More details for this conversion is provided in the function definition as comment. 3. The projecteur now initialize HID device correctly (Get rid of any device configuration by other programs). The projecteur also pings the device and check the the HID++ version supported by the device. Note: To connect logitech spotlight using bluetooth, press top button and the last button till the led light starting flashing. The spotlight device can now be paired with computer. --- src/device-vibration.cc | 8 +-- src/device.cc | 117 ++++++++++++++++++++++++++++++++++------ src/device.h | 30 ++++++++--- src/devicescan.cc | 12 ++--- src/devicescan.h | 2 - src/main.cc | 8 +-- src/spotlight.cc | 25 ++++++++- 7 files changed, 159 insertions(+), 43 deletions(-) diff --git a/src/device-vibration.cc b/src/device-vibration.cc index d1120460..d9abb0e8 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -20,8 +20,6 @@ #include #include -DECLARE_LOGGING_CATEGORY(device) - // ------------------------------------------------------------------------------------------------- namespace { constexpr int numTimers = 3; @@ -423,12 +421,10 @@ void VibrationSettingsWidget::sendVibrateCommand() // Spotlight: // len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1a, 0x00, 0xe8, 0x80}; + const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1a, vlen, 0xe8, vint}; - const auto res = m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); - if (res != sizeof(vibrateCmd)) { - logWarn(device) << "Could not write vibrate command to device socket."; - } + m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); } diff --git a/src/device.cc b/src/device.cc index 2e91037f..777557a7 100644 --- a/src/device.cc +++ b/src/device.cc @@ -64,8 +64,8 @@ bool DeviceConnection::removeSubDevice(const QString& path) } // ------------------------------------------------------------------------------------------------- -SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode) - : m_details(path, type, mode) {} +SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) + : m_details(path, type, mode, busType) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; @@ -118,7 +118,7 @@ QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { // ------------------------------------------------------------------------------------------------- SubEventConnection::SubEventConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly) {} + : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly, BusType::Unknown) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd, @@ -153,6 +153,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } auto connection = std::make_shared(Token{}, sd.deviceFile); + connection->m_details.busType = dc.deviceId().busType; if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; @@ -207,7 +208,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: // ------------------------------------------------------------------------------------------------- SubHidrawConnection::SubHidrawConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} + : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite, BusType::Unknown) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd, @@ -252,15 +253,15 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca } auto connection = std::make_shared(Token{}, sd.deviceFile); + connection->m_details.busType = dc.deviceId().busType; fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; } - // For now vibration is only supported for the Logitech Spotlight (USB) - // TODO A more generic approach - if (dc.deviceId().vendorId == 0x46d && dc.deviceId().productId == 0xc53e) { + // For now vibration is only supported for the Logitech Spotlight (USB and Bluetooth) + if (dc.deviceId().vendorId == 0x46d && (dc.deviceId().productId == 0xc53e || dc.deviceId().productId == 0xb503)) { connection->m_details.deviceFlags |= DeviceFlag::Vibrate; } @@ -281,33 +282,119 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca connection->m_details.phys = sd.phys; connection->disableWrite(); // disable write notifier + connection->initSubDevice(); return connection; } // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg) +void SubDeviceConnection::initSubDevice() +{ + struct timespec ts; + int msec = 50; + ts.tv_sec = msec / 1000; + ts.tv_nsec = (msec % 1000) * 1000000; + + resetSubDevice(ts); + + // Add other configuration to enable features in device + // like enabling on Next and back button on hold functionality. + // No intialization needed for Event Sub device +} + +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::resetSubDevice(struct timespec delay) +{ + // Ping spotlight device for checking if is online + pingSubDevice(); + ::nanosleep(&delay, &delay); + + // Reset device: Get rid of any device configuration by other programs + // Reset USB dongle + if (m_details.busType == BusType::Usb) { + {const uint8_t data[] = {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);} + ::nanosleep(&delay, &delay); + {const uint8_t data[] = {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00}; + sendData(data, sizeof(data), false);} + ::nanosleep(&delay, &delay); + } + + // Reset spotlight device + {const uint8_t data[] = {0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);} + ::nanosleep(&delay, &delay); +} + +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::pingSubDevice() +{ + const uint8_t pingCmd[] = {0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}; + sendData(pingCmd, sizeof(pingCmd), false); +} + +// ------------------------------------------------------------------------------------------------- +ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDeviceOnline) { ssize_t res = -1; - bool isValidMsg = (hidppMsg.length() == 7 && hidppMsg.at(0) == 0x10); // HID++ short message - isValidMsg = isValidMsg || (hidppMsg.length() == 20 && hidppMsg.at(0) == 0x11); // HID++ long message + + // If the message have 0xff as second byte, it is meant for USB dongle hence, + // should not be send when device is connected on bluetooth. + // + // + // Logitech Spotlight (USB) can receive data in two different length. + // 1. Short (10 byte long starting with 0x10) + // 2. Long (20 byte long starting with 0x11) + // However, bluetooth connection only accepts data in long (20 byte) packets. + // For converting standard short length data to long length data, change the first byte to 0x11 and + // pad the end of message with 0x00 to acheive the length of 20. + + QByteArray _hidppMsg(hidppMsg); + if (m_details.busType == BusType::Bluetooth) { + if (static_cast(hidppMsg.at(1)) == 0xff){ + logDebug(hid) << "Invalid packet" << hidppMsg.toHex() << "for spotlight connected on bluetooth."; + return res; + } + + if (hidppMsg.at(0) == 0x10) { + _hidppMsg.clear(); + _hidppMsg.append(0x11); + _hidppMsg.append(hidppMsg.mid(1)); + QByteArray padding(20 - _hidppMsg.length(), 0); + _hidppMsg.append(padding); + } + } + + bool isValidMsg = (_hidppMsg.length() == 7 && _hidppMsg.at(0) == 0x10); // HID++ short message + isValidMsg = isValidMsg || (_hidppMsg.length() == 20 && _hidppMsg.at(0) == 0x11); // HID++ long message + // If checkDeviceOnline is true then do not send the packet if device is not online/active. + if (checkDeviceOnline) { + isValidMsg = isValidMsg && isOnline(); + } if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite && m_writeNotifier && isValidMsg) { enableWrite(); const auto notifier = socketWriteNotifier(); - res = ::write(notifier->socket(), hidppMsg.data(), hidppMsg.length()); - logDebug(hid) << "Write" << hidppMsg.toHex() << "to" << path(); + res = ::write(notifier->socket(), _hidppMsg.data(), _hidppMsg.length()); disableWrite(); } + if (res == _hidppMsg.length()) { + logDebug(hid) << "Write" << _hidppMsg.toHex() << "to" << path(); + } else { + logWarn(hid) << "Writing to" << path() << "failed."; + if (checkDeviceOnline && !isOnline()) { + logInfo(hid) << "The device is not active. Activate it by pressing any button on device."; + } + } + return res; } - // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen) +ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline) { const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen); - return sendData(hidppMsgArr); + return sendData(hidppMsgArr, checkDeviceOnline); } diff --git a/src/device.h b/src/device.h index b4a90c89..96612540 100644 --- a/src/device.h +++ b/src/device.h @@ -9,24 +9,29 @@ #include +// ------------------------------------------------------------------------------------------------- +// Bus on which device is connected +enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; + // ------------------------------------------------------------------------------------------------- struct DeviceId { uint16_t vendorId = 0; uint16_t productId = 0; + BusType busType = BusType::Unknown; QString phys; // should be sufficient to differentiate between two devices of the same type // - not tested, don't have two devices of any type currently. inline bool operator==(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } inline bool operator!=(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } inline bool operator<(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } }; @@ -90,15 +95,17 @@ ENUM(DeviceFlag, DeviceFlags) // ----------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { - SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode) - : type(type), mode(mode), devicePath(path) {} + SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) + : type(type), mode(mode), busType(busType), devicePath(path) {} ConnectionType type; ConnectionMode mode; + BusType busType; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString phys; QString devicePath; + float hidProtocolVer = -1; // set after ping to HID sub-device; If positive then Hidraw device is online. }; // ------------------------------------------------------------------------------------------------- @@ -130,8 +137,15 @@ class SubDeviceConnection : public QObject void disableWrite(); // disable sending data void enableWrite(); // enable sending data - ssize_t sendData(const QByteArray& hidppMsg); // Send HID++ Message to HIDraw connection - ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen); // Send HID++ Message to HIDraw connection + // HID++ specific functions + void initSubDevice(); + void resetSubDevice(struct timespec delay); + void pingSubDevice(); + bool isOnline(){return (m_details.hidProtocolVer > 0);}; + void setHIDProtocol(float p){m_details.hidProtocolVer = p;}; + float getHIDProtocol(){return m_details.hidProtocolVer;}; + ssize_t sendData(const QByteArray& hidppMsg, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection + ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection auto type() const { return m_details.type; }; auto mode() const { return m_details.mode; }; @@ -145,7 +159,7 @@ class SubDeviceConnection : public QObject QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device protected: - SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode); + SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType); SubDeviceConnectionDetails m_details; std::shared_ptr m_inputMapper; // shared input mapper from parent device. diff --git a/src/devicescan.cc b/src/devicescan.cc index 84374bee..4782010f 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -135,8 +135,8 @@ namespace { const auto busType = ids.size() ? ids[0].toUShort(nullptr, 16) : 0; switch (busType) { - case BUS_USB: spotlightDevice.busType = DeviceScan::Device::BusType::Usb; break; - case BUS_BLUETOOTH: spotlightDevice.busType = DeviceScan::Device::BusType::Bluetooth; break; + case BUS_USB: spotlightDevice.id.busType = BusType::Usb; break; + case BUS_BLUETOOTH: spotlightDevice.id.busType = BusType::Bluetooth; break; } spotlightDevice.id.vendorId = ids.size() > 1 ? ids[1].toUShort(nullptr, 16) : 0; spotlightDevice.id.productId = ids.size() > 2 ? ids[2].toUShort(nullptr, 16) : 0; @@ -154,7 +154,6 @@ namespace { } return spotlightDevice; } - } namespace DeviceScan { @@ -257,10 +256,9 @@ namespace DeviceScan { } } - // For now: only check for hidraw sub-devices that have support for custom "proprietary" - // functionality/protocol with Projecteur built in. - // TODO check if _Projecteur_ supports additional "proprietary" device protocol features.. - if (eventSubDeviceCount > 0) continue; + // Spotlight (Bluetooth) have hidraw interface in the same folder. However + // for other connection, it has separate folder for hidraw device and input device. + if (!(rootDevice.id.busType == BusType::Bluetooth) && eventSubDeviceCount > 0) continue; // Iterate over 'hidraw' sub-dircectory, check for hidraw device node const QFileInfo hidrawSubdir(QDir(hidIt.filePath()).filePath("hidraw")); diff --git a/src/devicescan.h b/src/devicescan.h index ea1b0c6c..04fd407c 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -31,12 +31,10 @@ namespace DeviceScan }; struct Device { // Structure for device scan results - enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; const QString& getName() const { return userName.size() ? userName : name; } QString name; QString userName; DeviceId id; - BusType busType = BusType::Unknown; std::vector subDevices; }; diff --git a/src/main.cc b/src/main.cc index 850b93e2..a316f843 100644 --- a/src/main.cc +++ b/src/main.cc @@ -231,9 +231,9 @@ int main(int argc, char *argv[]) << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); - const auto busTypeToString = [](DeviceScan::Device::BusType type) -> QString { - if (type == DeviceScan::Device::BusType::Usb) return "USB"; - if (type == DeviceScan::Device::BusType::Bluetooth) return "Bluetooth"; + const auto busTypeToString = [](BusType type) -> QString { + if (type == BusType::Usb) return "USB"; + if (type == BusType::Bluetooth) return "Bluetooth"; return "unknown"; }; @@ -266,7 +266,7 @@ int main(int argc, char *argv[]) print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); print() << " " << "productId: " << logging::hexId(device.id.productId); print() << " " << "phys: " << device.id.phys; - print() << " " << "busType: " << busTypeToString(device.busType); + print() << " " << "busType: " << busTypeToString(device.id.busType); print() << " " << "devices: " << subDeviceList.join(", "); print() << " " << "readable: " << (allReadable ? "true" : "false"); print() << " " << "writable: " << (allWriteable ? "true" : "false"); diff --git a/src/spotlight.cc b/src/spotlight.cc index 23d75531..2d6449f2 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -308,8 +308,31 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } return; } + + // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) + if (!(readVal.at(0) == 0x10 || readVal.at(0) == 0x11)) { + return; + } + logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - // TODO: Process Logitech HIDPP message + + if (readVal.at(0) == 0x11) // Logitech HIDPP LONG message: 20 byte long + { + if (readVal.at(2) == 0x00 && readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) // response to ping + { + auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; + logDebug(hid) << connection.path() << "is online with protocol version" << protocolVer ; + connection.setHIDProtocol(protocolVer); + } + if (readVal.at(2) == 0x04) { // Logitech spotlight presenter unit got online. + connection.initSubDevice(); + } + // TODO: Process other packets + + if (readVal.at(2) == 0x09 && readVal.at(3) == 0x1a) { + logDebug(hid) << "Device acknowledged a vibration event."; + } + } } // ------------------------------------------------------------------------------------------------- From 2b97ee35909ffa07cc51c33352ad4878ee32846c Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sun, 18 Jul 2021 02:03:23 +0530 Subject: [PATCH 022/110] Added support for reading battery information from device The software can now read battery information from the device. Additionally, device details (including battery information is now shown in Details tab inside Devices Tab). --- src/device.cc | 58 +++++++++++++++---- src/device.h | 33 ++++++++++- src/deviceswidget.cc | 135 +++++++++++++++++++++++++++++++++++++++++-- src/deviceswidget.h | 6 ++ src/spotlight.cc | 27 +++++++-- src/spotlight.h | 1 + 6 files changed, 235 insertions(+), 25 deletions(-) diff --git a/src/device.cc b/src/device.cc index 777557a7..4ebbaede 100644 --- a/src/device.cc +++ b/src/device.cc @@ -63,6 +63,29 @@ bool DeviceConnection::removeSubDevice(const QString& path) return false; } +// ------------------------------------------------------------------------------------------------- +void DeviceConnection::queryBatteryStatus() +{ + if (subDeviceCount() > 0) { + for (const auto& sd: subDevices()) { + if (sd.second->type() == ConnectionType::Hidraw && sd.second->mode() == ConnectionMode::ReadWrite) { + sd.second->queryBatteryStatus(); + } + } + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceConnection::setBatteryInfo(QByteArray batteryData) +{ + if (batteryData.length() == 3) + { + m_batteryInfo.status = static_cast(batteryData.at(2)); + m_batteryInfo.currentLevel = static_cast(batteryData.at(0)); + m_batteryInfo.nextReportedLevel = static_cast(batteryData.at(1)); + } +} + // ------------------------------------------------------------------------------------------------- SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) : m_details(path, type, mode, busType) {} @@ -296,6 +319,8 @@ void SubDeviceConnection::initSubDevice() resetSubDevice(ts); + queryBatteryStatus(); + // Add other configuration to enable features in device // like enabling on Next and back button on hold functionality. // No intialization needed for Event Sub device @@ -332,6 +357,19 @@ void SubDeviceConnection::pingSubDevice() sendData(pingCmd, sizeof(pingCmd), false); } +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::queryBatteryStatus() +{ + // if we make battery feature request packet by sending {0x11, 0x01, 0x00, 0x0d, 0x10, 0x00 ... padded by 0x00} + // batteryFeatureID is the fifth byte obtained by as device response. + // batteryFeatureID may differ for different logitech devices and may change after firmware update. + // last checked, batteryFeatureID was 0x06 for logitech spotlight. + + const uint8_t batteryFeatureID = 0x06; + const uint8_t batteryCmd[] = {0x10, 0x01, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; + sendData(batteryCmd, sizeof(batteryCmd), false); +} + // ------------------------------------------------------------------------------------------------- ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDeviceOnline) { @@ -366,26 +404,24 @@ ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDevi bool isValidMsg = (_hidppMsg.length() == 7 && _hidppMsg.at(0) == 0x10); // HID++ short message isValidMsg = isValidMsg || (_hidppMsg.length() == 20 && _hidppMsg.at(0) == 0x11); // HID++ long message + // If checkDeviceOnline is true then do not send the packet if device is not online/active. - if (checkDeviceOnline) { - isValidMsg = isValidMsg && isOnline(); + if (checkDeviceOnline && !isOnline()) { + logInfo(hid) << "The device is not active. Activate it by pressing any button on device."; + return res; } if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite - && m_writeNotifier && isValidMsg) - { + && m_writeNotifier && isValidMsg) { enableWrite(); const auto notifier = socketWriteNotifier(); res = ::write(notifier->socket(), _hidppMsg.data(), _hidppMsg.length()); disableWrite(); - } - if (res == _hidppMsg.length()) { - logDebug(hid) << "Write" << _hidppMsg.toHex() << "to" << path(); - } else { - logWarn(hid) << "Writing to" << path() << "failed."; - if (checkDeviceOnline && !isOnline()) { - logInfo(hid) << "The device is not active. Activate it by pressing any button on device."; + if (res == _hidppMsg.length()) { + logDebug(hid) << "Write" << _hidppMsg.toHex() << "to" << path(); + } else { + logWarn(hid) << "Writing to" << path() << "failed."; } } diff --git a/src/device.h b/src/device.h index 96612540..9a41ab38 100644 --- a/src/device.h +++ b/src/device.h @@ -13,6 +13,22 @@ // Bus on which device is connected enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; +// ------------------------------------------------------------------------------------------------- +enum class BatteryStatus : uint8_t {Discharging = 0x00, + Charging = 0x01, + AlmostFull = 0x02, + Full = 0x03, + SlowCharging = 0x04, + InvalidBattery = 0x05, + ThermalError = 0x06}; + +struct BatteryInfo +{ + uint8_t currentLevel = 0; + uint8_t nextReportedLevel = 0; + BatteryStatus status = BatteryStatus::Discharging; +}; + // ------------------------------------------------------------------------------------------------- struct DeviceId { @@ -65,6 +81,11 @@ class DeviceConnection : public QObject void addSubDevice(std::shared_ptr); bool removeSubDevice(const QString& path); const auto& subDevices() { return m_subDeviceConnections; } + void queryBatteryStatus(); + auto getBatteryInfo(){return m_batteryInfo;}; + +public slots: + void setBatteryInfo(QByteArray batteryData); signals: void subDeviceConnected(const DeviceId& id, const QString& path); @@ -78,6 +99,7 @@ class DeviceConnection : public QObject QString m_deviceName; std::shared_ptr m_inputMapper; ConnectionMap m_subDeviceConnections; + BatteryInfo m_batteryInfo; }; // ------------------------------------------------------------------------------------------------- @@ -141,9 +163,10 @@ class SubDeviceConnection : public QObject void initSubDevice(); void resetSubDevice(struct timespec delay); void pingSubDevice(); - bool isOnline(){return (m_details.hidProtocolVer > 0);}; - void setHIDProtocol(float p){m_details.hidProtocolVer = p;}; - float getHIDProtocol(){return m_details.hidProtocolVer;}; + bool isOnline() {return (m_details.hidProtocolVer > 0);}; + void setHIDProtocol(float p) {m_details.hidProtocolVer = p;}; + float getHIDProtocol() {return m_details.hidProtocolVer;}; + void queryBatteryStatus(); ssize_t sendData(const QByteArray& hidppMsg, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection @@ -200,4 +223,8 @@ class SubHidrawConnection : public SubDeviceConnection const DeviceConnection& dc); SubHidrawConnection(Token, const QString& path); + +signals: + void receivedBatteryInfo(QByteArray batteryData); + void receivedPingResponse(); }; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 080a5747..412aa237 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -159,9 +159,14 @@ QWidget* DevicesWidget::createDevicesWidget(Settings* settings, Spotlight* spotl m_vibrationSettingsWidget->setSubDeviceConnection(conn.get()); } + m_deviceDetailsTabWidget = createDeviceInfoWidget(spotlight); + tabWidget->addTab(m_deviceDetailsTabWidget, tr("Details")); + connect(this, &DevicesWidget::currentDeviceChanged, this, [vibrateConn=std::move(vibrateConn), tabWidget, settings, spotlight, this] (const DeviceId& devId) { + const auto idx = tabWidget->indexOf(m_deviceDetailsTabWidget); + if (idx >= 0) tabWidget->removeTab(idx); if (const auto conn = vibrateConn(devId)) { if (m_timerTabWidget == nullptr) { m_timerTabWidget = createTimerTabWidget(settings, spotlight); @@ -176,20 +181,140 @@ QWidget* DevicesWidget::createDevicesWidget(Settings* settings, Spotlight* spotl if (idx >= 0) tabWidget->removeTab(idx); m_vibrationSettingsWidget->setSubDeviceConnection(nullptr); } + // ensure that Details tab is last tab + tabWidget->addTab(m_deviceDetailsTabWidget, tr("Details")); + + tabWidget->setCurrentIndex(0); }); return dw; } // ------------------------------------------------------------------------------------------------- -QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* /*spotlight*/) +void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) +{ + auto updateBatteryInfo = [this, spotlight]() { + auto curDeviceId = currentDeviceId(); + if (curDeviceId == invalidDeviceId) + return; + auto dc = spotlight->deviceConnection(curDeviceId); + dc->queryBatteryStatus(); + }; + + auto getDeviceDetails = [this, spotlight]() { + QString deviceDetails; + auto curDeviceId = currentDeviceId(); + if (curDeviceId == invalidDeviceId) + return tr("No Device Connected"); + auto dc = spotlight->deviceConnection(curDeviceId); + + const auto busTypeToString = [](BusType type) -> QString { + if (type == BusType::Usb) return "USB"; + if (type == BusType::Bluetooth) return "Bluetooth"; + return "Unknown"; + }; + + const QStringList subDeviceList = [dc](){ + QStringList subDeviceList; + auto accessText = [](ConnectionMode m){ + if (m == ConnectionMode::ReadOnly) return "ReadOnly"; + if (m == ConnectionMode::WriteOnly) return "WriteOnly"; + if (m == ConnectionMode::ReadWrite) return "ReadWrite"; + return "Unknown Access"; + }; + // report special flags set by program (like vibration and others) + auto flagText = [](DeviceFlag f){ + QStringList flagList; + if (!!(f & DeviceFlag::Vibrate)) flagList.push_back("Vibration"); + return flagList; + }; + for (const auto& sd: dc->subDevices()) { + if (sd.second->path().size()) { + auto sds = sd.second; + auto flagInfo = flagText(sds->flags()); + subDeviceList.push_back(tr("%1\t[%2, %3, %4]").arg(sds->path(), + accessText(sds->mode()), + sds->isGrabbed()?"Grabbed":"", + flagInfo.isEmpty()?"":"Supports: " + flagInfo.join("; ") + )); + } + } + return subDeviceList; + }(); + auto batteryStatusText = [](BatteryStatus d){ + if (d == BatteryStatus::Discharging) return "Discharging"; + if (d == BatteryStatus::Charging) return "Charging"; + if (d == BatteryStatus::AlmostFull) return "Almost Full"; + if (d == BatteryStatus::Full) return "Full Charge"; + if (d == BatteryStatus::SlowCharging) return "Slow Charging"; + if (d == BatteryStatus::InvalidBattery || d == BatteryStatus::ThermalError) { + return "Battery Problem/Invalid Battery"; + }; + return ""; + }; + + auto batteryInfoText = [dc, batteryStatusText](){ + auto sDevices = dc->subDevices(); + const bool isOnline = std::any_of(sDevices.cbegin(), sDevices.cend(), + [](const auto& sd){ + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + sd.second->isOnline()); + }); + if (isOnline) { + auto batteryInfo= dc->getBatteryInfo(); + // Only show battery percent while discharging. + // Other cases, device do not report battery percentage correctly. + if (batteryInfo.status == BatteryStatus::Discharging) { + return tr("%1\% - %2% (%3)").arg( + QString::number(batteryInfo.currentLevel), + QString::number(batteryInfo.nextReportedLevel), + batteryStatusText(batteryInfo.status)); + } else { + return tr("%3").arg(batteryStatusText(batteryInfo.status)); + } + } else { + return tr("Device not active. Press any key on device to update."); + } + }; + + deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); + deviceDetails += tr("VendorId:\t%1\n").arg(logging::hexId(dc->deviceId().vendorId)); + deviceDetails += tr("ProductId:\t%1\n").arg(logging::hexId(dc->deviceId().productId)); + deviceDetails += tr("Phys:\t\t%1\n").arg(dc->deviceId().phys); + deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); + deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); + deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); + + return deviceDetails; + }; + + + updateBatteryInfo(); + if (m_deviceDetailsTextEdit) { + QTimer::singleShot(1000, this, [this, getDeviceDetails](){m_deviceDetailsTextEdit->setText(getDeviceDetails());}); + } +} + +// ------------------------------------------------------------------------------------------------- +QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) { const auto diWidget = new QWidget(this); const auto layout = new QHBoxLayout(diWidget); - layout->addStretch(1); - layout->addWidget(new QLabel(tr("Not yet implemented"), this)); - layout->addStretch(1); - diWidget->setDisabled(true); + if (!m_deviceDetailsTextEdit) m_deviceDetailsTextEdit = new QTextEdit(this); + m_deviceDetailsTextEdit->setReadOnly(true); + m_deviceDetailsTextEdit->setText(""); + + updateDeviceDetails(spotlight); + + connect(m_updateDeviceDetailsTimer, &QTimer::timeout, this, [this, spotlight](){updateDeviceDetails(spotlight);}); + m_updateDeviceDetailsTimer->start(900000); // Update every 15 minutes + + connect(this, &DevicesWidget::currentDeviceChanged, this, [this, spotlight](){updateDeviceDetails(spotlight);}); + connect(spotlight, &Spotlight::deviceActivated, this, + [this, spotlight](const DeviceId& d){if (d==currentDeviceId()){ updateDeviceDetails(spotlight);};}); + + layout->addWidget(m_deviceDetailsTextEdit); return diWidget; } diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 6e3c865c..08b5c6c8 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -3,6 +3,8 @@ #include #include +#include +#include struct DeviceId; class InputMapper; @@ -30,9 +32,13 @@ class DevicesWidget : public QWidget QWidget* createInputMapperWidget(Settings* settings, Spotlight* spotlight); QWidget* createDeviceInfoWidget(Spotlight* spotlight); QWidget* createTimerTabWidget(Settings* settings, Spotlight* spotlight); + void updateDeviceDetails(Spotlight* spotlight); QComboBox* m_devicesCombo = nullptr; QWidget* m_timerTabWidget = nullptr; + QWidget* m_deviceDetailsTabWidget = nullptr; + QTextEdit* m_deviceDetailsTextEdit = nullptr; + QTimer* m_updateDeviceDetailsTimer = new QTimer(this); VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; QPointer m_inputMapper; }; diff --git a/src/spotlight.cc b/src/spotlight.cc index 2d6449f2..677f2f7e 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -133,7 +133,13 @@ int Spotlight::connectDevices() if (addInputEventHandler(devCon)) return devCon; } else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); - if(addHIDInputHandler(hidCon)) return hidCon; + if(addHIDInputHandler(hidCon)) { + connect(hidCon.get(), &SubHidrawConnection::receivedBatteryInfo, + dc.get(), &DeviceConnection::setBatteryInfo); + connect(hidCon.get(), &SubHidrawConnection::receivedPingResponse, + dc.get(), [this, dc](){emit deviceActivated(dc->deviceId(), dc->deviceName());}); + return hidCon; + } } return std::shared_ptr(); }(); @@ -318,15 +324,24 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) if (readVal.at(0) == 0x11) // Logitech HIDPP LONG message: 20 byte long { - if (readVal.at(2) == 0x00 && readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) // response to ping - { - auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; - logDebug(hid) << connection.path() << "is online with protocol version" << protocolVer ; - connection.setHIDProtocol(protocolVer); + if (readVal.at(2) == 0x00) { + if (readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) { // response to ping + auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; + logDebug(hid) << connection.path() << "is online with protocol version" << protocolVer ; + connection.setHIDProtocol(protocolVer); + if (connection.isOnline()) emit connection.receivedPingResponse(); + } } + if (readVal.at(2) == 0x04) { // Logitech spotlight presenter unit got online. connection.initSubDevice(); } + + if (readVal.at(2) == 0x06 && readVal.at(3) == 0x0d) { // Battery information packet + QByteArray batteryData(readVal.mid(4, 3)); + emit connection.receivedBatteryInfo(batteryData); + } + // TODO: Process other packets if (readVal.at(2) == 0x09 && readVal.at(3) == 0x1a) { diff --git a/src/spotlight.h b/src/spotlight.h index 235d1fe2..a075bbf4 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -44,6 +44,7 @@ class Spotlight : public QObject signals: void deviceConnected(const DeviceId& id, const QString& name); void deviceDisconnected(const DeviceId& id, const QString& name); + void deviceActivated(const DeviceId& id, const QString& name); void subDeviceConnected(const DeviceId& id, const QString& name, const QString& path); void subDeviceDisconnected(const DeviceId& id, const QString& name, const QString& path); void anySpotlightDeviceConnectedChanged(bool connected); From cf79056d93eedda2e572100fe534d9ca4b561954 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Fri, 16 Jul 2021 20:11:41 +0530 Subject: [PATCH 023/110] Enable Hidraw interface on spotlight bluetooth connection This commit enables hidraw interface on logitech spotlight connected via bluetooth. Three major changes are 1. DeviceId (in device.h) now include the information about the bus on which spotlight is connected (USB/Bluetooth). 2. sendData function (device.cc) for hidraw on bluetooth modifies the data before sending it. The bluetooth hid need the data in 20 byte long packages; smaller packet of 7 byte length is not allowed on bluetooth connection. More details for this conversion is provided in the function definition as comment. 3. The projecteur now initialize HID device correctly (Get rid of any device configuration by other programs). The projecteur also pings the device and check the the HID++ version supported by the device. Note: To connect logitech spotlight using bluetooth, press top button and the last button till the led light starting flashing. The spotlight device can now be paired with computer. --- README.md | 2 +- src/device-vibration.cc | 10 ++-- src/device.cc | 116 ++++++++++++++++++++++++++++++++++------ src/device.h | 29 +++++++--- src/devicescan.cc | 12 ++--- src/devicescan.h | 2 - src/main.cc | 8 +-- src/spotlight.cc | 37 ++++++++++++- 8 files changed, 169 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 48d8e90a..5bc076d5 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ So here it is: a Linux application for the Logitech Spotlight. * Button mapping: * Map any button on the device to (almost) any keyboard combination. * Switch between (cycle through) custom spotlight presets. -* Vibration (Timer) Support for the Logitech Spotlight (USB) +* Vibration (Timer) Support for the Logitech Spotlight * Usable without a presenter device (e.g. for online presentations) ### Screenshots diff --git a/src/device-vibration.cc b/src/device-vibration.cc index d1120460..3dc40f6e 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -20,8 +20,6 @@ #include #include -DECLARE_LOGGING_CATEGORY(device) - // ------------------------------------------------------------------------------------------------- namespace { constexpr int numTimers = 3; @@ -423,12 +421,10 @@ void VibrationSettingsWidget::sendVibrateCommand() // Spotlight: // len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1a, 0x00, 0xe8, 0x80}; + const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); - const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1a, vlen, 0xe8, vint}; + const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1d, vlen, 0xe8, vint}; - const auto res = m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); - if (res != sizeof(vibrateCmd)) { - logWarn(device) << "Could not write vibrate command to device socket."; - } + m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); } diff --git a/src/device.cc b/src/device.cc index 2e91037f..6216299c 100644 --- a/src/device.cc +++ b/src/device.cc @@ -6,6 +6,7 @@ #include "logging.h" #include +#include #include #include @@ -64,8 +65,8 @@ bool DeviceConnection::removeSubDevice(const QString& path) } // ------------------------------------------------------------------------------------------------- -SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode) - : m_details(path, type, mode) {} +SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) + : m_details(path, type, mode, busType) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; @@ -118,7 +119,7 @@ QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { // ------------------------------------------------------------------------------------------------- SubEventConnection::SubEventConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly) {} + : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly, BusType::Unknown) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd, @@ -153,6 +154,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } auto connection = std::make_shared(Token{}, sd.deviceFile); + connection->m_details.busType = dc.deviceId().busType; if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; @@ -207,7 +209,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: // ------------------------------------------------------------------------------------------------- SubHidrawConnection::SubHidrawConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} + : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite, BusType::Unknown) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd, @@ -252,15 +254,15 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca } auto connection = std::make_shared(Token{}, sd.deviceFile); + connection->m_details.busType = dc.deviceId().busType; fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; } - // For now vibration is only supported for the Logitech Spotlight (USB) - // TODO A more generic approach - if (dc.deviceId().vendorId == 0x46d && dc.deviceId().productId == 0xc53e) { + // For now vibration is only supported for the Logitech Spotlight (USB and Bluetooth) + if (dc.deviceId().vendorId == 0x46d && (dc.deviceId().productId == 0xc53e || dc.deviceId().productId == 0xb503)) { connection->m_details.deviceFlags |= DeviceFlag::Vibrate; } @@ -281,33 +283,113 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca connection->m_details.phys = sd.phys; connection->disableWrite(); // disable write notifier + connection->initSubDevice(); return connection; } // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg) +void SubDeviceConnection::initSubDevice() +{ + int msgCount = 1, delay_ms = 20; + + // Ping spotlight device for checking if is online + QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); + msgCount++; + + // Reset device: get rid of any device configuration by other programs ------- + // Reset USB dongle + if (m_details.busType == BusType::Usb) { + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + // Turn off software bit and keep the wireless notification bit on + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + } + + // Reset spotlight device + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + // Device Resetting complete ------------------------------------------------- + + // Add other configuration to enable features in device + // like enabling on Next and back button on hold functionality. + // No intialization needed for Event Sub device +} + +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::pingSubDevice() +{ + const uint8_t pingCmd[] = {0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}; + sendData(pingCmd, sizeof(pingCmd), false); +} + +// ------------------------------------------------------------------------------------------------- +ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDeviceOnline) { ssize_t res = -1; - bool isValidMsg = (hidppMsg.length() == 7 && hidppMsg.at(0) == 0x10); // HID++ short message - isValidMsg = isValidMsg || (hidppMsg.length() == 20 && hidppMsg.at(0) == 0x11); // HID++ long message + + // If the message have 0xff as second byte, it is meant for USB dongle hence, + // should not be send when device is connected on bluetooth. + // + // + // Logitech Spotlight (USB) can receive data in two different length. + // 1. Short (10 byte long starting with 0x10) + // 2. Long (20 byte long starting with 0x11) + // However, bluetooth connection only accepts data in long (20 byte) packets. + // For converting standard short length data to long length data, change the first byte to 0x11 and + // pad the end of message with 0x00 to acheive the length of 20. + + QByteArray _hidppMsg(hidppMsg); + if (m_details.busType == BusType::Bluetooth) { + if (static_cast(hidppMsg.at(1)) == 0xff){ + logDebug(hid) << "Invalid packet" << hidppMsg.toHex() << "for spotlight connected on bluetooth."; + return res; + } + + if (hidppMsg.at(0) == 0x10) { + _hidppMsg.clear(); + _hidppMsg.append(0x11); + _hidppMsg.append(hidppMsg.mid(1)); + QByteArray padding(20 - _hidppMsg.length(), 0); + _hidppMsg.append(padding); + } + } + + bool isValidMsg = (_hidppMsg.length() == 7 && _hidppMsg.at(0) == 0x10); // HID++ short message + isValidMsg = isValidMsg || (_hidppMsg.length() == 20 && _hidppMsg.at(0) == 0x11); // HID++ long message + + // If checkDeviceOnline is true then do not send the packet if device is not online/active. + if (checkDeviceOnline && !isOnline()) { + logInfo(hid) << "The device is not active. Activate it by pressing any button on device."; + return res; + } if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite - && m_writeNotifier && isValidMsg) - { + && m_writeNotifier && isValidMsg) { enableWrite(); const auto notifier = socketWriteNotifier(); - res = ::write(notifier->socket(), hidppMsg.data(), hidppMsg.length()); - logDebug(hid) << "Write" << hidppMsg.toHex() << "to" << path(); + res = ::write(notifier->socket(), _hidppMsg.data(), _hidppMsg.length()); disableWrite(); + + if (res == _hidppMsg.length()) { + logDebug(hid) << "Write" << _hidppMsg.toHex() << "to" << path(); + } else { + logWarn(hid) << "Writing to" << path() << "failed."; + } } return res; } - // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen) +ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline) { const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen); - return sendData(hidppMsgArr); + return sendData(hidppMsgArr, checkDeviceOnline); } diff --git a/src/device.h b/src/device.h index b4a90c89..e2192498 100644 --- a/src/device.h +++ b/src/device.h @@ -9,24 +9,29 @@ #include +// ------------------------------------------------------------------------------------------------- +// Bus on which device is connected +enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; + // ------------------------------------------------------------------------------------------------- struct DeviceId { uint16_t vendorId = 0; uint16_t productId = 0; + BusType busType = BusType::Unknown; QString phys; // should be sufficient to differentiate between two devices of the same type // - not tested, don't have two devices of any type currently. inline bool operator==(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } inline bool operator!=(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } inline bool operator<(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } }; @@ -90,15 +95,17 @@ ENUM(DeviceFlag, DeviceFlags) // ----------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { - SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode) - : type(type), mode(mode), devicePath(path) {} + SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) + : type(type), mode(mode), busType(busType), devicePath(path) {} ConnectionType type; ConnectionMode mode; + BusType busType; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString phys; QString devicePath; + float hidProtocolVer = -1; // set after ping to HID sub-device; If positive then Hidraw device is online. }; // ------------------------------------------------------------------------------------------------- @@ -130,8 +137,14 @@ class SubDeviceConnection : public QObject void disableWrite(); // disable sending data void enableWrite(); // enable sending data - ssize_t sendData(const QByteArray& hidppMsg); // Send HID++ Message to HIDraw connection - ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen); // Send HID++ Message to HIDraw connection + // HID++ specific functions + void initSubDevice(); + void pingSubDevice(); + bool isOnline() { return (m_details.hidProtocolVer > 0); }; + void setHIDProtocol(float p) { m_details.hidProtocolVer = p; }; + float getHIDProtocol() { return m_details.hidProtocolVer; }; + ssize_t sendData(const QByteArray& hidppMsg, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection + ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection auto type() const { return m_details.type; }; auto mode() const { return m_details.mode; }; @@ -145,7 +158,7 @@ class SubDeviceConnection : public QObject QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device protected: - SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode); + SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType); SubDeviceConnectionDetails m_details; std::shared_ptr m_inputMapper; // shared input mapper from parent device. diff --git a/src/devicescan.cc b/src/devicescan.cc index 84374bee..4782010f 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -135,8 +135,8 @@ namespace { const auto busType = ids.size() ? ids[0].toUShort(nullptr, 16) : 0; switch (busType) { - case BUS_USB: spotlightDevice.busType = DeviceScan::Device::BusType::Usb; break; - case BUS_BLUETOOTH: spotlightDevice.busType = DeviceScan::Device::BusType::Bluetooth; break; + case BUS_USB: spotlightDevice.id.busType = BusType::Usb; break; + case BUS_BLUETOOTH: spotlightDevice.id.busType = BusType::Bluetooth; break; } spotlightDevice.id.vendorId = ids.size() > 1 ? ids[1].toUShort(nullptr, 16) : 0; spotlightDevice.id.productId = ids.size() > 2 ? ids[2].toUShort(nullptr, 16) : 0; @@ -154,7 +154,6 @@ namespace { } return spotlightDevice; } - } namespace DeviceScan { @@ -257,10 +256,9 @@ namespace DeviceScan { } } - // For now: only check for hidraw sub-devices that have support for custom "proprietary" - // functionality/protocol with Projecteur built in. - // TODO check if _Projecteur_ supports additional "proprietary" device protocol features.. - if (eventSubDeviceCount > 0) continue; + // Spotlight (Bluetooth) have hidraw interface in the same folder. However + // for other connection, it has separate folder for hidraw device and input device. + if (!(rootDevice.id.busType == BusType::Bluetooth) && eventSubDeviceCount > 0) continue; // Iterate over 'hidraw' sub-dircectory, check for hidraw device node const QFileInfo hidrawSubdir(QDir(hidIt.filePath()).filePath("hidraw")); diff --git a/src/devicescan.h b/src/devicescan.h index ea1b0c6c..04fd407c 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -31,12 +31,10 @@ namespace DeviceScan }; struct Device { // Structure for device scan results - enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; const QString& getName() const { return userName.size() ? userName : name; } QString name; QString userName; DeviceId id; - BusType busType = BusType::Unknown; std::vector subDevices; }; diff --git a/src/main.cc b/src/main.cc index 850b93e2..a316f843 100644 --- a/src/main.cc +++ b/src/main.cc @@ -231,9 +231,9 @@ int main(int argc, char *argv[]) << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); - const auto busTypeToString = [](DeviceScan::Device::BusType type) -> QString { - if (type == DeviceScan::Device::BusType::Usb) return "USB"; - if (type == DeviceScan::Device::BusType::Bluetooth) return "Bluetooth"; + const auto busTypeToString = [](BusType type) -> QString { + if (type == BusType::Usb) return "USB"; + if (type == BusType::Bluetooth) return "Bluetooth"; return "unknown"; }; @@ -266,7 +266,7 @@ int main(int argc, char *argv[]) print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); print() << " " << "productId: " << logging::hexId(device.id.productId); print() << " " << "phys: " << device.id.phys; - print() << " " << "busType: " << busTypeToString(device.busType); + print() << " " << "busType: " << busTypeToString(device.id.busType); print() << " " << "devices: " << subDeviceList.join(", "); print() << " " << "readable: " << (allReadable ? "true" : "false"); print() << " " << "writable: " << (allWriteable ? "true" : "false"); diff --git a/src/spotlight.cc b/src/spotlight.cc index 23d75531..6e16979d 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -308,8 +308,43 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } return; } + + // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) + if (!(readVal.at(0) == 0x10 || readVal.at(0) == 0x11)) { + return; + } + logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - // TODO: Process Logitech HIDPP message + + if (readVal.at(0) == 0x10) // Logitech HIDPP SHORT message: 7 byte long + { + if (readVal.at(2) == 0x41 && !!(readVal.at(3) & 0x04 && + !(readVal.at(4) & 1<<6))) { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. + // currently it is off as I observed that device send two online packet + // one with 0x10 and other with 0x11. Currently initsubDevice is triggered + // on 0x11 packet. + //connection.initSubDevice(); + } + } + + if (readVal.at(0) == 0x11) // Logitech HIDPP LONG message: 20 byte long + { + if (readVal.at(2) == 0x00 && readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) // response to ping + { + auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; + logDebug(hid) << connection.path() << "is online with protocol version" << protocolVer ; + connection.setHIDProtocol(protocolVer); + } + if (readVal.at(2) == 0x04 && readVal.at(4) ==0x01 && + readVal.at(5) == 0x01 && readVal.at(6) == 0x01) { // Logitech spotlight presenter unit got online. + connection.initSubDevice(); + } + // TODO: Process other packets + + if (readVal.at(2) == 0x09 && readVal.at(3) == 0x1d) { + logDebug(hid) << "Device acknowledged a vibration event."; + } + } } // ------------------------------------------------------------------------------------------------- From 4a57c4439793dd34e4e90d1dd7c5cab5cc586f62 Mon Sep 17 00:00:00 2001 From: Jahn Date: Mon, 19 Jul 2021 08:57:17 +0200 Subject: [PATCH 024/110] Added additional udev rule for Logitech hidraw sub-devices. --- 55-projecteur.rules.in | 4 +++- src/spotlight.cc | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/55-projecteur.rules.in b/55-projecteur.rules.in index aff67495..dacce1f6 100644 --- a/55-projecteur.rules.in +++ b/55-projecteur.rules.in @@ -12,9 +12,11 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c53e", MODE="0660 # Rule fot the Logitech Spotlight when connected via Bluetooth # Updated rule, thanks to Torsten Maehne (https://github.com/maehne) SUBSYSTEMS=="input", ENV{LIBINPUT_DEVICE_GROUP}="5/46d/b503*", ATTRS{name}=="SPOTLIGHT*", MODE="0660", TAG+="uaccess" +# Additional rule for Bluetooth sub-devices (hidraw) +SUBSYSTEMS=="hid", KERNELS=="0005:046D:B503.*", MODE="0660", TAG+="uaccess" # Additional supported Bluetooth devices @EXTRA_BLUETOOTH_UDEV_RULES@ -# Rules for uninput: Essential for creating a virtual input device that +# Rules for uninput: Essential for creating a virtual input device that # Projecteur use for forwarding device events to the system after grabbing it KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput" diff --git a/src/spotlight.cc b/src/spotlight.cc index 6e16979d..6eaac467 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -123,7 +123,12 @@ int Spotlight::connectDevices() const bool anyConnectedBefore = anySpotlightDeviceConnected(); for (const auto& scanSubDevice : dev.subDevices) { - if (!scanSubDevice.deviceReadable) continue; + if (!scanSubDevice.deviceReadable) + { + logWarn(device) << tr("Sub-device not readable: %1 (%2:%3) %4") + .arg(dc->deviceName(), hexId(dev.id.vendorId), hexId(dev.id.productId), scanSubDevice.deviceFile); + continue; + } if (dc->hasSubDevice(scanSubDevice.deviceFile)) continue; std::shared_ptr subDeviceConnection = From 65b91bb1d1489c674640bfea51085a8a8781f3f2 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Fri, 16 Jul 2021 20:11:41 +0530 Subject: [PATCH 025/110] Enable Hidraw interface on spotlight bluetooth connection This commit enables hidraw interface on logitech spotlight connected via bluetooth. Three major changes are 1. DeviceId (in device.h) now include the information about the bus on which spotlight is connected (USB/Bluetooth). 2. sendData function (device.cc) for hidraw on bluetooth modifies the data before sending it. The bluetooth hid need the data in 20 byte long packages; smaller packet of 7 byte length is not allowed on bluetooth connection. More details for this conversion is provided in the function definition as comment. 3. The projecteur now initialize HID device correctly (Get rid of any device configuration by other programs). The projecteur also pings the device and check the the HID++ version supported by the device. Note: To connect logitech spotlight using bluetooth, press top button and the last button till the led light starting flashing. The spotlight device can now be paired with computer. --- README.md | 2 +- src/device-vibration.cc | 10 ++-- src/device.cc | 116 ++++++++++++++++++++++++++++++++++------ src/device.h | 30 ++++++++--- src/devicescan.cc | 12 ++--- src/devicescan.h | 2 - src/main.cc | 8 +-- src/spotlight.cc | 37 ++++++++++++- 8 files changed, 170 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 48d8e90a..5bc076d5 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ So here it is: a Linux application for the Logitech Spotlight. * Button mapping: * Map any button on the device to (almost) any keyboard combination. * Switch between (cycle through) custom spotlight presets. -* Vibration (Timer) Support for the Logitech Spotlight (USB) +* Vibration (Timer) Support for the Logitech Spotlight * Usable without a presenter device (e.g. for online presentations) ### Screenshots diff --git a/src/device-vibration.cc b/src/device-vibration.cc index d1120460..3dc40f6e 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -20,8 +20,6 @@ #include #include -DECLARE_LOGGING_CATEGORY(device) - // ------------------------------------------------------------------------------------------------- namespace { constexpr int numTimers = 3; @@ -423,12 +421,10 @@ void VibrationSettingsWidget::sendVibrateCommand() // Spotlight: // len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1a, 0x00, 0xe8, 0x80}; + const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); - const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1a, vlen, 0xe8, vint}; + const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1d, vlen, 0xe8, vint}; - const auto res = m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); - if (res != sizeof(vibrateCmd)) { - logWarn(device) << "Could not write vibrate command to device socket."; - } + m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); } diff --git a/src/device.cc b/src/device.cc index 2e91037f..6216299c 100644 --- a/src/device.cc +++ b/src/device.cc @@ -6,6 +6,7 @@ #include "logging.h" #include +#include #include #include @@ -64,8 +65,8 @@ bool DeviceConnection::removeSubDevice(const QString& path) } // ------------------------------------------------------------------------------------------------- -SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode) - : m_details(path, type, mode) {} +SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) + : m_details(path, type, mode, busType) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; @@ -118,7 +119,7 @@ QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { // ------------------------------------------------------------------------------------------------- SubEventConnection::SubEventConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly) {} + : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly, BusType::Unknown) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd, @@ -153,6 +154,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } auto connection = std::make_shared(Token{}, sd.deviceFile); + connection->m_details.busType = dc.deviceId().busType; if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; @@ -207,7 +209,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: // ------------------------------------------------------------------------------------------------- SubHidrawConnection::SubHidrawConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} + : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite, BusType::Unknown) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd, @@ -252,15 +254,15 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca } auto connection = std::make_shared(Token{}, sd.deviceFile); + connection->m_details.busType = dc.deviceId().busType; fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; } - // For now vibration is only supported for the Logitech Spotlight (USB) - // TODO A more generic approach - if (dc.deviceId().vendorId == 0x46d && dc.deviceId().productId == 0xc53e) { + // For now vibration is only supported for the Logitech Spotlight (USB and Bluetooth) + if (dc.deviceId().vendorId == 0x46d && (dc.deviceId().productId == 0xc53e || dc.deviceId().productId == 0xb503)) { connection->m_details.deviceFlags |= DeviceFlag::Vibrate; } @@ -281,33 +283,113 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca connection->m_details.phys = sd.phys; connection->disableWrite(); // disable write notifier + connection->initSubDevice(); return connection; } // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg) +void SubDeviceConnection::initSubDevice() +{ + int msgCount = 1, delay_ms = 20; + + // Ping spotlight device for checking if is online + QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); + msgCount++; + + // Reset device: get rid of any device configuration by other programs ------- + // Reset USB dongle + if (m_details.busType == BusType::Usb) { + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + // Turn off software bit and keep the wireless notification bit on + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + } + + // Reset spotlight device + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + // Device Resetting complete ------------------------------------------------- + + // Add other configuration to enable features in device + // like enabling on Next and back button on hold functionality. + // No intialization needed for Event Sub device +} + +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::pingSubDevice() +{ + const uint8_t pingCmd[] = {0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}; + sendData(pingCmd, sizeof(pingCmd), false); +} + +// ------------------------------------------------------------------------------------------------- +ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDeviceOnline) { ssize_t res = -1; - bool isValidMsg = (hidppMsg.length() == 7 && hidppMsg.at(0) == 0x10); // HID++ short message - isValidMsg = isValidMsg || (hidppMsg.length() == 20 && hidppMsg.at(0) == 0x11); // HID++ long message + + // If the message have 0xff as second byte, it is meant for USB dongle hence, + // should not be send when device is connected on bluetooth. + // + // + // Logitech Spotlight (USB) can receive data in two different length. + // 1. Short (10 byte long starting with 0x10) + // 2. Long (20 byte long starting with 0x11) + // However, bluetooth connection only accepts data in long (20 byte) packets. + // For converting standard short length data to long length data, change the first byte to 0x11 and + // pad the end of message with 0x00 to acheive the length of 20. + + QByteArray _hidppMsg(hidppMsg); + if (m_details.busType == BusType::Bluetooth) { + if (static_cast(hidppMsg.at(1)) == 0xff){ + logDebug(hid) << "Invalid packet" << hidppMsg.toHex() << "for spotlight connected on bluetooth."; + return res; + } + + if (hidppMsg.at(0) == 0x10) { + _hidppMsg.clear(); + _hidppMsg.append(0x11); + _hidppMsg.append(hidppMsg.mid(1)); + QByteArray padding(20 - _hidppMsg.length(), 0); + _hidppMsg.append(padding); + } + } + + bool isValidMsg = (_hidppMsg.length() == 7 && _hidppMsg.at(0) == 0x10); // HID++ short message + isValidMsg = isValidMsg || (_hidppMsg.length() == 20 && _hidppMsg.at(0) == 0x11); // HID++ long message + + // If checkDeviceOnline is true then do not send the packet if device is not online/active. + if (checkDeviceOnline && !isOnline()) { + logInfo(hid) << "The device is not active. Activate it by pressing any button on device."; + return res; + } if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite - && m_writeNotifier && isValidMsg) - { + && m_writeNotifier && isValidMsg) { enableWrite(); const auto notifier = socketWriteNotifier(); - res = ::write(notifier->socket(), hidppMsg.data(), hidppMsg.length()); - logDebug(hid) << "Write" << hidppMsg.toHex() << "to" << path(); + res = ::write(notifier->socket(), _hidppMsg.data(), _hidppMsg.length()); disableWrite(); + + if (res == _hidppMsg.length()) { + logDebug(hid) << "Write" << _hidppMsg.toHex() << "to" << path(); + } else { + logWarn(hid) << "Writing to" << path() << "failed."; + } } return res; } - // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen) +ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline) { const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen); - return sendData(hidppMsgArr); + return sendData(hidppMsgArr, checkDeviceOnline); } diff --git a/src/device.h b/src/device.h index b4a90c89..dd3ac44f 100644 --- a/src/device.h +++ b/src/device.h @@ -9,24 +9,29 @@ #include +// ------------------------------------------------------------------------------------------------- +// Bus on which device is connected +enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; + // ------------------------------------------------------------------------------------------------- struct DeviceId { uint16_t vendorId = 0; uint16_t productId = 0; + BusType busType = BusType::Unknown; QString phys; // should be sufficient to differentiate between two devices of the same type // - not tested, don't have two devices of any type currently. inline bool operator==(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } inline bool operator!=(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } inline bool operator<(const DeviceId& rhs) const { - return std::tie(vendorId, productId, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.phys); + return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); } }; @@ -90,15 +95,17 @@ ENUM(DeviceFlag, DeviceFlags) // ----------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { - SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode) - : type(type), mode(mode), devicePath(path) {} + SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) + : type(type), mode(mode), busType(busType), devicePath(path) {} ConnectionType type; ConnectionMode mode; + BusType busType; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString phys; QString devicePath; + float hidProtocolVer = -1; // set after ping to HID sub-device; If positive then Hidraw device is online. }; // ------------------------------------------------------------------------------------------------- @@ -130,8 +137,15 @@ class SubDeviceConnection : public QObject void disableWrite(); // disable sending data void enableWrite(); // enable sending data - ssize_t sendData(const QByteArray& hidppMsg); // Send HID++ Message to HIDraw connection - ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen); // Send HID++ Message to HIDraw connection + // HID++ specific functions + void initSubDevice(); + void pingSubDevice(); + bool isOnline() { return (m_details.busType == BusType::Bluetooth || + m_details.hidProtocolVer > 0); }; + void setHIDProtocol(float p) { m_details.hidProtocolVer = p; }; + float getHIDProtocol() { return m_details.hidProtocolVer; }; + ssize_t sendData(const QByteArray& hidppMsg, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection + ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection auto type() const { return m_details.type; }; auto mode() const { return m_details.mode; }; @@ -145,7 +159,7 @@ class SubDeviceConnection : public QObject QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device protected: - SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode); + SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType); SubDeviceConnectionDetails m_details; std::shared_ptr m_inputMapper; // shared input mapper from parent device. diff --git a/src/devicescan.cc b/src/devicescan.cc index 84374bee..4782010f 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -135,8 +135,8 @@ namespace { const auto busType = ids.size() ? ids[0].toUShort(nullptr, 16) : 0; switch (busType) { - case BUS_USB: spotlightDevice.busType = DeviceScan::Device::BusType::Usb; break; - case BUS_BLUETOOTH: spotlightDevice.busType = DeviceScan::Device::BusType::Bluetooth; break; + case BUS_USB: spotlightDevice.id.busType = BusType::Usb; break; + case BUS_BLUETOOTH: spotlightDevice.id.busType = BusType::Bluetooth; break; } spotlightDevice.id.vendorId = ids.size() > 1 ? ids[1].toUShort(nullptr, 16) : 0; spotlightDevice.id.productId = ids.size() > 2 ? ids[2].toUShort(nullptr, 16) : 0; @@ -154,7 +154,6 @@ namespace { } return spotlightDevice; } - } namespace DeviceScan { @@ -257,10 +256,9 @@ namespace DeviceScan { } } - // For now: only check for hidraw sub-devices that have support for custom "proprietary" - // functionality/protocol with Projecteur built in. - // TODO check if _Projecteur_ supports additional "proprietary" device protocol features.. - if (eventSubDeviceCount > 0) continue; + // Spotlight (Bluetooth) have hidraw interface in the same folder. However + // for other connection, it has separate folder for hidraw device and input device. + if (!(rootDevice.id.busType == BusType::Bluetooth) && eventSubDeviceCount > 0) continue; // Iterate over 'hidraw' sub-dircectory, check for hidraw device node const QFileInfo hidrawSubdir(QDir(hidIt.filePath()).filePath("hidraw")); diff --git a/src/devicescan.h b/src/devicescan.h index ea1b0c6c..04fd407c 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -31,12 +31,10 @@ namespace DeviceScan }; struct Device { // Structure for device scan results - enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; const QString& getName() const { return userName.size() ? userName : name; } QString name; QString userName; DeviceId id; - BusType busType = BusType::Unknown; std::vector subDevices; }; diff --git a/src/main.cc b/src/main.cc index 850b93e2..a316f843 100644 --- a/src/main.cc +++ b/src/main.cc @@ -231,9 +231,9 @@ int main(int argc, char *argv[]) << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); - const auto busTypeToString = [](DeviceScan::Device::BusType type) -> QString { - if (type == DeviceScan::Device::BusType::Usb) return "USB"; - if (type == DeviceScan::Device::BusType::Bluetooth) return "Bluetooth"; + const auto busTypeToString = [](BusType type) -> QString { + if (type == BusType::Usb) return "USB"; + if (type == BusType::Bluetooth) return "Bluetooth"; return "unknown"; }; @@ -266,7 +266,7 @@ int main(int argc, char *argv[]) print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); print() << " " << "productId: " << logging::hexId(device.id.productId); print() << " " << "phys: " << device.id.phys; - print() << " " << "busType: " << busTypeToString(device.busType); + print() << " " << "busType: " << busTypeToString(device.id.busType); print() << " " << "devices: " << subDeviceList.join(", "); print() << " " << "readable: " << (allReadable ? "true" : "false"); print() << " " << "writable: " << (allWriteable ? "true" : "false"); diff --git a/src/spotlight.cc b/src/spotlight.cc index 23d75531..6e16979d 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -308,8 +308,43 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } return; } + + // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) + if (!(readVal.at(0) == 0x10 || readVal.at(0) == 0x11)) { + return; + } + logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - // TODO: Process Logitech HIDPP message + + if (readVal.at(0) == 0x10) // Logitech HIDPP SHORT message: 7 byte long + { + if (readVal.at(2) == 0x41 && !!(readVal.at(3) & 0x04 && + !(readVal.at(4) & 1<<6))) { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. + // currently it is off as I observed that device send two online packet + // one with 0x10 and other with 0x11. Currently initsubDevice is triggered + // on 0x11 packet. + //connection.initSubDevice(); + } + } + + if (readVal.at(0) == 0x11) // Logitech HIDPP LONG message: 20 byte long + { + if (readVal.at(2) == 0x00 && readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) // response to ping + { + auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; + logDebug(hid) << connection.path() << "is online with protocol version" << protocolVer ; + connection.setHIDProtocol(protocolVer); + } + if (readVal.at(2) == 0x04 && readVal.at(4) ==0x01 && + readVal.at(5) == 0x01 && readVal.at(6) == 0x01) { // Logitech spotlight presenter unit got online. + connection.initSubDevice(); + } + // TODO: Process other packets + + if (readVal.at(2) == 0x09 && readVal.at(3) == 0x1d) { + logDebug(hid) << "Device acknowledged a vibration event."; + } + } } // ------------------------------------------------------------------------------------------------- From 02aae73e396387d50d668e2a3bae918c085ed8fe Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Tue, 20 Jul 2021 02:25:08 +0530 Subject: [PATCH 026/110] Added processing of connection broke event --- src/device.cc | 24 +++++++++++++++--------- src/device.h | 3 ++- src/deviceswidget.cc | 20 ++++++++++++-------- src/spotlight.cc | 18 ++++++++++-------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/device.cc b/src/device.cc index 827c003d..cec71bfc 100644 --- a/src/device.cc +++ b/src/device.cc @@ -77,14 +77,14 @@ void DeviceConnection::queryBatteryStatus() } // ------------------------------------------------------------------------------------------------- -void DeviceConnection::setBatteryInfo(QByteArray batteryData) +void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) { if (batteryData.length() == 3) { // battery percent is only meaningful when battery is discharging. However, save them anyway. - m_batteryInfo.currentLevel = static_cast(batteryData.at(0) <= 100? batteryData.at(0): 100); - m_batteryInfo.nextReportedLevel = static_cast(batteryData.at(1) <= 100? batteryData.at(1): 100); - m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x06)? batteryData.at(2): 0x06); + m_batteryInfo.currentLevel = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0): 100); + m_batteryInfo.nextReportedLevel = static_cast(batteryData.at(1) <= 100 ? batteryData.at(1): 100); + m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x06) ? batteryData.at(2): 0x06); } } @@ -288,6 +288,7 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca // For now vibration is only supported for the Logitech Spotlight (USB and Bluetooth) if (dc.deviceId().vendorId == 0x46d && (dc.deviceId().productId == 0xc53e || dc.deviceId().productId == 0xb503)) { connection->m_details.deviceFlags |= DeviceFlag::Vibrate; + connection->m_details.deviceFlags |= DeviceFlag::HasBattery; } // Create read and write socket notifiers @@ -320,11 +321,14 @@ void SubDeviceConnection::pingSubDevice() // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::setHIDProtocol(float version) { - logDebug(hid) << path() << "is online with protocol version" << version ; + if (version > 0) { + logDebug(hid) << path() << "is online with protocol version" << version ; + } else { + logDebug(hid) << "HID Device with path" << path() << "got deactivated."; + } m_details.hidProtocolVer = version; } - // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::initSubDevice() { @@ -376,9 +380,11 @@ void SubDeviceConnection::queryBatteryStatus() // batteryFeatureID may differ for different logitech devices and may change after firmware update. // last checked, batteryFeatureID was 0x06 for logitech spotlight. - const uint8_t batteryFeatureID = 0x06; - const uint8_t batteryCmd[] = {0x10, 0x01, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; - sendData(batteryCmd, sizeof(batteryCmd), false); + if (!!(flags() & DeviceFlag::HasBattery)) { + const uint8_t batteryFeatureID = 0x06; + const uint8_t batteryCmd[] = {0x10, 0x01, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; + sendData(batteryCmd, sizeof(batteryCmd), false); + } } // ------------------------------------------------------------------------------------------------- diff --git a/src/device.h b/src/device.h index 3cc8e8f2..944e515c 100644 --- a/src/device.h +++ b/src/device.h @@ -85,7 +85,7 @@ class DeviceConnection : public QObject auto getBatteryInfo(){return m_batteryInfo;}; public slots: - void setBatteryInfo(QByteArray batteryData); + void setBatteryInfo(const QByteArray& batteryData); signals: void subDeviceConnected(const DeviceId& id, const QString& path); @@ -112,6 +112,7 @@ enum class DeviceFlag : uint32_t { KeyEvents = 1 << 4, Vibrate = 1 << 16, + HasBattery = 1 << 17, }; ENUM(DeviceFlag, DeviceFlags) diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 8fea77e7..28833683 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -253,14 +253,13 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) return ""; }; - auto batteryInfoText = [dc, batteryStatusText](){ - auto sDevices = dc->subDevices(); + auto sDevices = dc->subDevices(); + auto batteryInfoText = [dc, batteryStatusText, sDevices](){ const bool isOnline = std::any_of(sDevices.cbegin(), sDevices.cend(), - [](const auto& sd){ - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - sd.second->isOnline()); - }); + [](const auto& sd){ + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + sd.second->isOnline());}); if (isOnline) { auto batteryInfo= dc->getBatteryInfo(); // Only show battery percent while discharging. @@ -277,6 +276,11 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) return tr("Device not active. Press any key on device to update."); } }; + const bool hasBattery = std::any_of(sDevices.cbegin(), sDevices.cend(), + [](const auto& sd){ + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + !!(sd.second->flags() & DeviceFlag::HasBattery));}); deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); deviceDetails += tr("VendorId:\t%1\n").arg(logging::hexId(dc->deviceId().vendorId)); @@ -284,7 +288,7 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) deviceDetails += tr("Phys:\t\t%1\n").arg(dc->deviceId().phys); deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); - deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); + if (hasBattery) deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); return deviceDetails; }; diff --git a/src/spotlight.cc b/src/spotlight.cc index 1afcd219..6f2a3275 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -329,12 +329,15 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) if (readVal.at(0) == 0x10) // Logitech HIDPP SHORT message: 7 byte long { - if (readVal.at(2) == 0x41 && !!(readVal.at(3) & 0x04) && - !(readVal.at(4) & 1<<6)) { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. - // currently it is off as I observed that device send two online packet - // one with 0x10 and other with 0x11. Currently initsubDevice is triggered - // on 0x11 packet. - //connection.initSubDevice(); + if (readVal.at(2) == 0x41 && !!(readVal.at(3) & 0x04)) { // wireless notification from USB dongle + if (readVal.at(4) & (1<<6)) { // connection between USB dongle and spotlight device broke + connection.setHIDProtocol(-1); + } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. + // currently it is off as I observed that device send two online packet + // one with 0x10 and other with 0x11. Currently initsubDevice is triggered + // on 0x11 packet. + //connection.initSubDevice(); + } } } @@ -348,8 +351,7 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } } - if (readVal.at(2) == 0x04 && readVal.at(4) ==0x01 && - readVal.at(5) == 0x01 && readVal.at(6) == 0x01) { // Logitech spotlight presenter unit got online. + if (readVal.at(2) == 0x04) { // Logitech spotlight presenter unit got online. connection.initSubDevice(); } From b177beedc721128297b41e632608c53dd201b41c Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Tue, 20 Jul 2021 20:43:33 +0530 Subject: [PATCH 027/110] Add support for querying HID++ 2.0 Feature Set Querying the Feature ID for different HID++ 2.0 feature set will help in making Projecteur future proof (of any firmware change in device). The official software query these Feature ID and keep using it until the firmware version on spotlight hardware changes. Additionally, by querying HID++ 2.0 Feature Set from device, we can be certain about device capabilities and we do not need to rely developer-defined flag system (which might be unreliable after firmware update on device). TODO: 1. Save the Feature Set to cache file and while connecting to device check the cache file for firmware verison. If firmware version matches load the Feature Set from the file. --- CMakeLists.txt | 1 + src/device.cc | 23 ++++++++++ src/device.h | 12 ++++++ src/hidpp.cc | 110 +++++++++++++++++++++++++++++++++++++++++++++++ src/hidpp.h | 45 +++++++++++++++++++ src/spotlight.cc | 5 +++ 6 files changed, 196 insertions(+) create mode 100644 src/hidpp.cc create mode 100644 src/hidpp.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 313e58f7..39fab8f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ add_executable(projecteur src/spotlight.cc src/spotlight.h src/spotshapes.cc src/spotshapes.h src/virtualdevice.h src/virtualdevice.cc + src/hidpp.cc src/hidpp.h resources.qrc qml/qml.qrc) target_include_directories(projecteur PRIVATE src) diff --git a/src/device.cc b/src/device.cc index 2e91037f..12909ca8 100644 --- a/src/device.cc +++ b/src/device.cc @@ -264,6 +264,29 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca connection->m_details.deviceFlags |= DeviceFlag::Vibrate; } + // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from device + if (dc.deviceId().vendorId == 0x46d) // Only check FeatureSet for Logitech devices + { + connection->m_featureSet = std::make_shared(dc.getFeatureSet()); + connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); + connection->m_featureSet->populateFeatureTable(); + if (connection->m_featureSet->getFeatureCount()) { + logDebug(hid) << "Loaded" << connection->m_featureSet->getFeatureCount() << "features for" << connection->path(); + if (connection->m_featureSet->supportFeatureCode(FeatureCode::PresenterControl)) { + connection->m_details.deviceFlags |= DeviceFlag::Vibrate; + logDebug(hid) << "SubDevice" << connection->path() << "reported Vibration capabilities."; + } + if (connection->m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus) || + connection->m_featureSet->supportFeatureCode(FeatureCode::BatteryVoltage) || + connection->m_featureSet->supportFeatureCode(FeatureCode::BatteryUnified)) { + connection->m_details.deviceFlags |= DeviceFlag::HasBattery; + logDebug(hid) << "SubDevice" << connection->path() << "can communicate battery information."; + } + } else { + logWarn(hid) << "Loading FeatureSet for" << connection->path() << "failed. Device might be inactive."; + } + } + // Create read and write socket notifiers connection->m_readNotifier = std::make_unique(devfd, QSocketNotifier::Read); QSocketNotifier* const readNotifier = connection->m_readNotifier.get(); diff --git a/src/device.h b/src/device.h index b4a90c89..cac35113 100644 --- a/src/device.h +++ b/src/device.h @@ -2,6 +2,7 @@ #pragma once #include "enum-helper.h" +#include "hidpp.h" #include @@ -37,6 +38,7 @@ class InputMapper; class QSocketNotifier; class SubDeviceConnection; class VirtualDevice; +class FeatureSet; // ----------------------------------------------------------------------------------------------- enum class ConnectionType : uint8_t { Event, Hidraw }; @@ -60,6 +62,7 @@ class DeviceConnection : public QObject void addSubDevice(std::shared_ptr); bool removeSubDevice(const QString& path); const auto& subDevices() { return m_subDeviceConnections; } + auto getFeatureSet() const { return m_featureSet; } signals: void subDeviceConnected(const DeviceId& id, const QString& path); @@ -73,6 +76,7 @@ class DeviceConnection : public QObject QString m_deviceName; std::shared_ptr m_inputMapper; ConnectionMap m_subDeviceConnections; + FeatureSet m_featureSet; }; // ------------------------------------------------------------------------------------------------- @@ -85,6 +89,7 @@ enum class DeviceFlag : uint32_t { KeyEvents = 1 << 4, Vibrate = 1 << 16, + HasBattery = 1 << 17, }; ENUM(DeviceFlag, DeviceFlags) @@ -186,4 +191,11 @@ class SubHidrawConnection : public SubDeviceConnection const DeviceConnection& dc); SubHidrawConnection(Token, const QString& path); + auto getFeatureSet () const { return m_featureSet; }; + +signals: + void receivedPingResponse(); + +protected: + std::shared_ptr m_featureSet = nullptr; }; diff --git a/src/hidpp.cc b/src/hidpp.cc new file mode 100644 index 00000000..090d0c30 --- /dev/null +++ b/src/hidpp.cc @@ -0,0 +1,110 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +#include "hidpp.h" +#include "logging.h" + +#include + +DECLARE_LOGGING_CATEGORY(hid) + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::populateFeatureTable(){ + if (m_fHIDDevice) { + auto getResponse = [this](QByteArray expectedBytes){ + QByteArray readVal(20, 0); + while(true) { + if(::read(m_fHIDDevice, readVal.data(), readVal.length())) { + //logInfo(hid) << "Received" << readVal.toHex() << "Expected" << expectedBytes.toHex(); + if (readVal.mid(1, 3) == expectedBytes) return readVal; + if (static_cast(readVal.at(2)) == 0x8f) return readVal; //Device not online + if (errno != EAGAIN) return QByteArray(20, 0x8f); + } + } + }; + // To get firmware details: first get Feature ID corresponding to Firmware feature code + // and then make final request to get firmware version using the obtained feature ID + uint8_t fwLSB = static_cast(static_cast(FeatureCode::FirmwareVersion) >> 8); //0x00 + uint8_t fwMSB = static_cast(static_cast(FeatureCode::FirmwareVersion)); //0x03 + uint8_t fwIDReq[] = {0x11, 0x01, 0x00, 0x0d, fwLSB, fwMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray fwReqArr(reinterpret_cast(fwIDReq), sizeof(fwIDReq)); + ::write(m_fHIDDevice, fwReqArr.data(), fwReqArr.length()); + auto response = getResponse(fwReqArr.mid(1, 3)); + if (static_cast(response.at(2)) == 0x8f) return; + uint8_t fwID = static_cast(response.at(4)); + + // Get the firmware version now + uint8_t fwVerReq[] = {0x11, 0x01, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray fwVerReqArr(reinterpret_cast(fwVerReq), sizeof(fwVerReq)); + ::write(m_fHIDDevice, fwVerReqArr.data(), fwVerReqArr.length()); + auto fwResponse1 = getResponse(fwVerReqArr.mid(1, 3)); + if (static_cast(fwResponse1.at(2)) == 0x8f) return; + + uint8_t fwVer2Req[] = {0x11, 0x01, fwID, 0x1d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray fwVer2ReqArr(reinterpret_cast(fwVer2Req), sizeof(fwVer2Req)); + ::write(m_fHIDDevice, fwVer2ReqArr.data(), fwVer2ReqArr.length()); + auto fwResponse2 = getResponse(fwVer2ReqArr.mid(1, 3)); + if (static_cast(fwResponse2.at(2)) == 0x8f) return; + //TODO:: make sense of fwResponse1 and fwResponse2 + + // TODO:: Read and write cache file + // if the firmware details match with cached file; then load the FeatureTable from file + // else read the entire feature table from the device + + + // For reading feature table from device + // first get the Feature Index for Feature Set + uint8_t fSetLSB = static_cast(static_cast(FeatureCode::FeatureSet) >> 8); //0x00 + uint8_t fSetMSB = static_cast(static_cast(FeatureCode::FeatureSet)); //0x01 + uint8_t featureSetIDReq[] = {0x11, 0x01, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray featureSetIDReqArr(reinterpret_cast(featureSetIDReq), sizeof(featureSetIDReq)); + ::write(m_fHIDDevice, featureSetIDReqArr.data(), featureSetIDReqArr.length()); + response = getResponse(featureSetIDReqArr.mid(1, 3)); + if (static_cast(response.at(2)) == 0x8f) return; + uint8_t featureSetID = static_cast(response.at(4)); + + // Get Number of features (except Root Feature) supported + uint8_t featureCountReq[] = {0x11, 0x01, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray featureCountReqArr(reinterpret_cast(featureCountReq), sizeof(featureCountReq)); + ::write(m_fHIDDevice, featureCountReqArr.data(), featureCountReqArr.length()); + response = getResponse(featureCountReqArr.mid(1, 3)); + if (static_cast(response.at(2)) == 0x8f) return; + uint8_t featureCount = static_cast(response.at(4)); + + // Root feature is supported by all HID++ 2.0 device and has a featureID of 0 + m_featureTable.insert({static_cast(FeatureCode::Root), 0x00}); + + // Read Feature Code for other featureIds from device + for (uint8_t featureId = 0x01; featureId <= featureCount; featureId++) { + const uint8_t data[] = {0x11, 0x01, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray dataArr(reinterpret_cast(data), sizeof(data)); + ::write(m_fHIDDevice, dataArr.data(), dataArr.length()); + response = getResponse(dataArr.mid(1, 3)); + if (static_cast(response.at(2)) == 0x8f) { + m_featureTable.clear(); + return; + } + uint16_t featureCode = (static_cast(response.at(4)) << 8) | static_cast(response.at(5)); + uint8_t featureType = static_cast(response.at(6)); + auto softwareHidden = (featureType & (1<<6)); + auto obsoleteFeature = (featureType & (1<<7)); + if (!(softwareHidden) && !(obsoleteFeature)) { + m_featureTable.insert({featureCode, featureId}); + } + } + } +} + +// ------------------------------------------------------------------------------------------------- +bool FeatureSet::supportFeatureCode(FeatureCode fc) +{ + auto featurePair = m_featureTable.find(static_cast(fc)); + return (featurePair != m_featureTable.end()); +} + +// ------------------------------------------------------------------------------------------------- +uint8_t FeatureSet::getFeatureID(FeatureCode fc) +{ + if (!supportFeatureCode(fc)) return 0x00; + + auto featurePair = m_featureTable.find(static_cast(fc)); + return featurePair->second; +} diff --git a/src/hidpp.h b/src/hidpp.h new file mode 100644 index 00000000..426d2fa9 --- /dev/null +++ b/src/hidpp.h @@ -0,0 +1,45 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +#pragma once + +#include + +#include +#include + +// Feature Codes important for Logitech Spotlight +enum class FeatureCode : uint16_t { + Root = 0x0000, + FeatureSet = 0x0001, + FirmwareVersion = 0x0003, + DeviceName = 0x0005, + Reset = 0x0020, + DFUControlSigned = 0x00c2, + BatteryStatus = 0x1000, + BatteryVoltage = 0x1001, + BatteryUnified = 0x1004, + PresenterControl = 0x1a00, + Sensor3D = 0x1a01, + ReprogramControlsV4 = 0x1b04, + WirelessDeviceStatus = 0x1db4, + SwapCancelButton = 0x2005, + PointerSpeed = 0x2205, +}; + + +class FeatureSet +{ +public: + FeatureSet() {}; + virtual ~FeatureSet() {} + + void setHIDDeviceFileDescriptor(int fd) { m_fHIDDevice = fd; }; + uint8_t getFeatureID(FeatureCode fc); + bool supportFeatureCode(FeatureCode fc); + auto getFeatureCount() { return m_featureTable.size(); } + void populateFeatureTable(); + bool hasFeatureTable(){ return !(m_featureTable.empty()); } + +protected: + std::map m_featureTable; + int m_fHIDDevice=0; +}; diff --git a/src/spotlight.cc b/src/spotlight.cc index 23d75531..bb7a0425 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -133,6 +133,11 @@ int Spotlight::connectDevices() if (addInputEventHandler(devCon)) return devCon; } else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); + if (dc->deviceId().vendorId == 0x46d && hidCon->getFeatureSet()->getFeatureCount() == 0) { + connect(hidCon.get(), &SubHidrawConnection::receivedPingResponse, + this, [this, hidCon](){ + removeDeviceConnection(hidCon->path());connectDevices();}); + } if(addHIDInputHandler(hidCon)) return hidCon; } return std::shared_ptr(); From b3a4df6987954964fc1830188a3f909ec86c7776 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Wed, 21 Jul 2021 15:31:42 +0530 Subject: [PATCH 028/110] Added HID++ Code and refactored accordingly --- src/device-vibration.cc | 8 +- src/device.cc | 200 ++++++++++++++++++------------------ src/device.h | 21 ++-- src/deviceswidget.cc | 8 +- src/deviceswidget.h | 1 + src/hidpp.cc | 217 ++++++++++++++++++++++++++++------------ src/hidpp.h | 29 +++++- src/preferencesdlg.cc | 1 - src/spotlight.cc | 40 ++++---- 9 files changed, 319 insertions(+), 206 deletions(-) diff --git a/src/device-vibration.cc b/src/device-vibration.cc index 96857e30..8329344f 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -419,12 +419,14 @@ void VibrationSettingsWidget::sendVibrateCommand() // for not only the Spotlight device. // // Spotlight: - // len intensity + // present + // controlID len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); - const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1d, vlen, 0xe8, vint}; + const uint8_t pcID = m_subDeviceConnection->getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); + const uint8_t vibrateCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, pcID, 0x1d, vlen, 0xe8, vint}; - m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); + if (pcID) m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); } diff --git a/src/device.cc b/src/device.cc index a6deefa0..615d4cde 100644 --- a/src/device.cc +++ b/src/device.cc @@ -25,7 +25,9 @@ namespace { // ------------------------------------------------------------------------------------------------- DeviceConnection::DeviceConnection(const DeviceId& id, const QString& name, std::shared_ptr vdev) - : m_deviceId(id), m_deviceName(name), m_inputMapper(std::make_shared(std::move(vdev))){ + : m_deviceId(id), + m_deviceName(name), + m_inputMapper(std::make_shared(std::move(vdev))){ m_featureSet = std::make_shared(FeatureSet()); } @@ -81,25 +83,18 @@ void DeviceConnection::queryBatteryStatus() // ------------------------------------------------------------------------------------------------- void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) { - bool test = m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus); - if (test && batteryData.length() == 3) + if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus) && batteryData.length() == 3) { - // battery percent is only meaningful when battery is discharging. However, save them anyway. + // Battery percent is only meaningful when battery is discharging. However, save them anyway. m_batteryInfo.currentLevel = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0): 100); m_batteryInfo.nextReportedLevel = static_cast(batteryData.at(1) <= 100 ? batteryData.at(1): 100); - m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x06) ? batteryData.at(2): 0x06); - } - if (m_featureSet->supportFeatureCode(FeatureCode::BatteryVoltage)) { - //TODO: Implement battery code for this feature - } - if (m_featureSet->supportFeatureCode(FeatureCode::BatteryUnified)) { - //TODO: Implement battery code for this feature + m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x07) ? batteryData.at(2): 0x07); } } // ------------------------------------------------------------------------------------------------- -SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) - : m_details(path, type, mode, busType) {} +SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode) + : m_details(path, type, mode) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; @@ -152,7 +147,7 @@ QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { // ------------------------------------------------------------------------------------------------- SubEventConnection::SubEventConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly, BusType::Unknown) {} + : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd, @@ -187,7 +182,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } auto connection = std::make_shared(Token{}, sd.deviceFile); - connection->m_details.busType = dc.deviceId().busType; + connection->m_deviceID = dc.deviceId(); if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; @@ -242,7 +237,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: // ------------------------------------------------------------------------------------------------- SubHidrawConnection::SubHidrawConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite, BusType::Unknown) {} + : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd, @@ -287,36 +282,15 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca } auto connection = std::make_shared(Token{}, sd.deviceFile); - connection->m_details.busType = dc.deviceId().busType; + connection->m_deviceID = dc.deviceId(); + connection->m_featureSet = dc.getFeatureSet(); + connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; } - // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from device - if (dc.deviceId().vendorId == 0x46d) // Only check FeatureSet for Logitech devices - { - connection->m_featureSet = dc.getFeatureSet(); - connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); - connection->m_featureSet->populateFeatureTable(); - if (connection->m_featureSet->getFeatureCount()) { - logDebug(hid) << "Loaded" << connection->m_featureSet->getFeatureCount() << "features for" << connection->path(); - if (connection->m_featureSet->supportFeatureCode(FeatureCode::PresenterControl)) { - connection->m_details.deviceFlags |= DeviceFlag::Vibrate; - logDebug(hid) << "SubDevice" << connection->path() << "reported Vibration capabilities."; - } - if (connection->m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus) || - connection->m_featureSet->supportFeatureCode(FeatureCode::BatteryVoltage) || - connection->m_featureSet->supportFeatureCode(FeatureCode::BatteryUnified)) { - connection->m_details.deviceFlags |= DeviceFlag::HasBattery; - logDebug(hid) << "SubDevice" << connection->path() << "can communicate battery information."; - } - } else { - logWarn(hid) << "Loading FeatureSet for" << connection->path() << "failed. Device might be inactive."; - } - } - // Create read and write socket notifiers connection->m_readNotifier = std::make_unique(devfd, QSocketNotifier::Read); QSocketNotifier* const readNotifier = connection->m_readNotifier.get(); @@ -333,7 +307,6 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca }); connection->m_details.phys = sd.phys; - connection->disableWrite(); // disable write notifier connection->initSubDevice(); return connection; } @@ -341,7 +314,8 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::pingSubDevice() { - const uint8_t pingCmd[] = {0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}; + uint8_t rootID = 0x00; // root ID is always 0x00 in any logitech device + const uint8_t pingCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, rootID, 0x1d, 0x00, 0x00, 0x5d}; sendData(pingCmd, sizeof(pingCmd), false); } @@ -359,57 +333,95 @@ void SubDeviceConnection::setHIDProtocol(float version) { void SubDeviceConnection::initSubDevice() { int msgCount = 0, delay_ms = 20; + if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite) + { + if (m_deviceID.vendorId == 0x46d) // Only check FeatureSet for Logitech devices + { + // Reset device: get rid of any device configuration by other programs ------- + if (m_deviceID.busType == BusType::Usb) + { + // Reset USB dongle + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_GET_FEATURE, 0x00, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + // Turn off software bit and keep the wireless notification bit on + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x01, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + // Initialize USB dongle + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x02, 0x02, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x09, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + } - // Reset device: get rid of any device configuration by other programs ------- - // Reset USB dongle - if (m_details.busType == BusType::Usb) { - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - - // Turn off software bit and keep the wireless notification bit on - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - } - - // Reset spotlight device - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - // Device Resetting complete ------------------------------------------------- - - if (m_details.busType == BusType::Usb) { - // Ping spotlight device for checking if is online - // the response will have the version for HID++ protocol. - QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); - msgCount++; - } else if (m_details.busType == BusType::Bluetooth) { - // Bluetooth connection mean HID++ v2.0+. - // Setting version to 6.4: same as USB connection. - setHIDProtocol(6.4); + // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from logitech device + disable(); + if (m_featureSet->getFeatureCount() == 0) m_featureSet->populateFeatureTable(); + if (m_featureSet->getFeatureCount()) { + logDebug(hid) << "Loaded" << m_featureSet->getFeatureCount() << "features for" << path(); + if (m_featureSet->supportFeatureCode(FeatureCode::PresenterControl)) { + m_details.deviceFlags |= DeviceFlag::Vibrate; + logDebug(hid) << "SubDevice" << path() << "reported Vibration capabilities."; + } + if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus)) { + m_details.deviceFlags |= DeviceFlag::ReportBattery; + logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; + } + } else { + logWarn(hid) << "Loading FeatureSet for" << path() << "failed. Device might be inactive."; + logInfo(hid) << "Press any button on device to activate it."; + } + if (m_readNotifier) m_readNotifier->setEnabled(true); + disableWrite(); + + // Reset spotlight device + if (m_featureSet->getFeatureCount()) { + const auto resetID = m_featureSet->getFeatureID(FeatureCode::Reset); + if (resetID) { + QTimer::singleShot(delay_ms*msgCount, this, [this, resetID](){ + const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, resetID, 0x1d, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + } + } + // Device Resetting complete ------------------------------------------------- + + if (m_deviceID.busType == BusType::Usb) { + // Ping spotlight device for checking if is online + // the response will have the version for HID++ protocol. + QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); + msgCount++; + } else if (m_deviceID.busType == BusType::Bluetooth) { + // Bluetooth connection mean HID++ v2.0+. + // Setting version to 6.4: same as USB connection. + setHIDProtocol(6.4); + } + // Add other configuration to enable features in device + // like enabling on Next and back button on hold functionality. + } } - - // Add other configuration to enable features in device - // like enabling on Next and back button on hold functionality. - // No intialization needed for Event Sub device + // No initialization for Event SubDevice } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::queryBatteryStatus() { - // if we make battery feature request packet by sending {0x11, 0x01, 0x00, 0x0d, 0x10, 0x00 ... padded by 0x00} - // batteryFeatureID is the fifth byte obtained by as device response. - // batteryFeatureID may differ for different logitech devices and may change after firmware update. - // last checked, batteryFeatureID was 0x06 for logitech spotlight. - - if (!!(flags() & DeviceFlag::HasBattery)) { - const uint8_t batteryFeatureID = 0x06; - const uint8_t batteryCmd[] = {0x10, 0x01, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; - sendData(batteryCmd, sizeof(batteryCmd), false); + if (!!(flags() & DeviceFlag::ReportBattery)) { + const uint8_t batteryFeatureID = m_featureSet->getFeatureID(FeatureCode::BatteryStatus); + if (batteryFeatureID) { + const uint8_t batteryCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; + sendData(batteryCmd, sizeof(batteryCmd), false); + } } } @@ -430,23 +442,19 @@ ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDevi // pad the end of message with 0x00 to acheive the length of 20. QByteArray _hidppMsg(hidppMsg); - if (m_details.busType == BusType::Bluetooth) { - if (static_cast(hidppMsg.at(1)) == 0xff){ + if (m_deviceID.busType == BusType::Bluetooth) { + if (static_cast(hidppMsg.at(1)) == MSG_TO_USB_RECEIVER){ logDebug(hid) << "Invalid packet" << hidppMsg.toHex() << "for spotlight connected on bluetooth."; return res; } - if (hidppMsg.at(0) == 0x10) { - _hidppMsg.clear(); - _hidppMsg.append(0x11); - _hidppMsg.append(hidppMsg.mid(1)); - QByteArray padding(20 - _hidppMsg.length(), 0); - _hidppMsg.append(padding); + if (hidppMsg.at(0) == HIDPP_SHORT_MSG) { + _hidppMsg = HIDPP::shortToLongMsg(hidppMsg); } } - bool isValidMsg = (_hidppMsg.length() == 7 && _hidppMsg.at(0) == 0x10); // HID++ short message - isValidMsg = isValidMsg || (_hidppMsg.length() == 20 && _hidppMsg.at(0) == 0x11); // HID++ long message + bool isValidMsg = (_hidppMsg.length() == 7 && _hidppMsg.at(0) == HIDPP_SHORT_MSG); // HID++ short message + isValidMsg = isValidMsg || (_hidppMsg.length() == 20 && _hidppMsg.at(0) == HIDPP_LONG_MSG); // HID++ long message // If checkDeviceOnline is true then do not send the packet if device is not online/active. if (checkDeviceOnline && !isOnline()) { diff --git a/src/device.h b/src/device.h index 40f44056..ec743985 100644 --- a/src/device.h +++ b/src/device.h @@ -46,13 +46,16 @@ class VirtualDevice; class FeatureSet; // ------------------------------------------------------------------------------------------------- +// Battery Status as returned on HID++ BatteryStatus feature code (0x1000) enum class BatteryStatus : uint8_t {Discharging = 0x00, Charging = 0x01, AlmostFull = 0x02, Full = 0x03, SlowCharging = 0x04, InvalidBattery = 0x05, - ThermalError = 0x06}; + ThermalError = 0x06, + ChargingError = 0x07 + }; struct BatteryInfo { @@ -116,18 +119,17 @@ enum class DeviceFlag : uint32_t { KeyEvents = 1 << 4, Vibrate = 1 << 16, - HasBattery = 1 << 17, + ReportBattery = 1 << 17, }; ENUM(DeviceFlag, DeviceFlags) // ----------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { - SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType) - : type(type), mode(mode), busType(busType), devicePath(path) {} + SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode) + : type(type), mode(mode), devicePath(path) {} ConnectionType type; ConnectionMode mode; - BusType busType; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString phys; @@ -165,6 +167,7 @@ class SubDeviceConnection : public QObject void enableWrite(); // enable sending data // HID++ specific functions + const auto& getFeatureSet () const { return m_featureSet; }; void initSubDevice(); void pingSubDevice(); bool isOnline() { return (m_details.hidProtocolVer > 0); }; @@ -186,12 +189,14 @@ class SubDeviceConnection : public QObject QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device protected: - SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode, BusType busType); + SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode); SubDeviceConnectionDetails m_details; std::shared_ptr m_inputMapper; // shared input mapper from parent device. std::unique_ptr m_readNotifier; std::unique_ptr m_writeNotifier; // only useful for Hidraw connections + std::shared_ptr m_featureSet = nullptr; + DeviceId m_deviceID; }; // ------------------------------------------------------------------------------------------------- @@ -227,12 +232,8 @@ class SubHidrawConnection : public SubDeviceConnection const DeviceConnection& dc); SubHidrawConnection(Token, const QString& path); - const auto& getFeatureSet () const { return m_featureSet; }; signals: void receivedBatteryInfo(QByteArray batteryData); void receivedPingResponse(); - -protected: - std::shared_ptr m_featureSet = nullptr; }; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 6fcc74f0..2a7add07 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -226,7 +226,7 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) auto flagText = [](DeviceFlag f){ QStringList flagList; if (!!(f & DeviceFlag::Vibrate)) flagList.push_back("Vibration"); - if (!!(f & DeviceFlag::HasBattery)) flagList.push_back("Report_Battery"); + if (!!(f & DeviceFlag::ReportBattery)) flagList.push_back("Report_Battery"); return flagList; }; for (const auto& sd: dc->subDevices()) { @@ -248,8 +248,8 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) if (d == BatteryStatus::AlmostFull) return "Almost Full"; if (d == BatteryStatus::Full) return "Full Charge"; if (d == BatteryStatus::SlowCharging) return "Slow Charging"; - if (d == BatteryStatus::InvalidBattery || d == BatteryStatus::ThermalError) { - return "Battery Problem/Invalid Battery"; + if (d == BatteryStatus::InvalidBattery || d == BatteryStatus::ThermalError || d == BatteryStatus::ChargingError) { + return "Charging Error"; }; return ""; }; @@ -281,7 +281,7 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) [](const auto& sd){ return (sd.second->type() == ConnectionType::Hidraw && sd.second->mode() == ConnectionMode::ReadWrite && - !!(sd.second->flags() & DeviceFlag::HasBattery));}); + !!(sd.second->flags() & DeviceFlag::ReportBattery));}); deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); deviceDetails += tr("VendorId:\t%1\n").arg(logging::hexId(dc->deviceId().vendorId)); diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 0713205e..8be1717e 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -5,6 +5,7 @@ #include #include #include +#include struct DeviceId; class InputMapper; diff --git a/src/hidpp.cc b/src/hidpp.cc index 090d0c30..e0f6bea2 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -4,81 +4,155 @@ #include +#include + DECLARE_LOGGING_CATEGORY(hid) // ------------------------------------------------------------------------------------------------- -void FeatureSet::populateFeatureTable(){ - if (m_fHIDDevice) { - auto getResponse = [this](QByteArray expectedBytes){ - QByteArray readVal(20, 0); - while(true) { - if(::read(m_fHIDDevice, readVal.data(), readVal.length())) { - //logInfo(hid) << "Received" << readVal.toHex() << "Expected" << expectedBytes.toHex(); - if (readVal.mid(1, 3) == expectedBytes) return readVal; - if (static_cast(readVal.at(2)) == 0x8f) return readVal; //Device not online - if (errno != EAGAIN) return QByteArray(20, 0x8f); - } - } - }; - // To get firmware details: first get Feature ID corresponding to Firmware feature code - // and then make final request to get firmware version using the obtained feature ID - uint8_t fwLSB = static_cast(static_cast(FeatureCode::FirmwareVersion) >> 8); //0x00 - uint8_t fwMSB = static_cast(static_cast(FeatureCode::FirmwareVersion)); //0x03 - uint8_t fwIDReq[] = {0x11, 0x01, 0x00, 0x0d, fwLSB, fwMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray fwReqArr(reinterpret_cast(fwIDReq), sizeof(fwIDReq)); - ::write(m_fHIDDevice, fwReqArr.data(), fwReqArr.length()); - auto response = getResponse(fwReqArr.mid(1, 3)); - if (static_cast(response.at(2)) == 0x8f) return; - uint8_t fwID = static_cast(response.at(4)); - - // Get the firmware version now - uint8_t fwVerReq[] = {0x11, 0x01, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +QByteArray FeatureSet::getResponseFromDevice(QByteArray expectedBytes) +{ + if (m_fdHIDDevice == -1) return QByteArray(); + + QByteArray readVal(20, 0); + int timeOut = 4; // time out just in case device did not reply; + // 4 seconds time out is used by other prorams like Solaar. + QTime timeOutTime = QTime::currentTime().addSecs(timeOut); + while(true) { + if(::read(m_fdHIDDevice, readVal.data(), readVal.length())) { + if (readVal.mid(1, 3) == expectedBytes) return readVal; + if (static_cast(readVal.at(2)) == 0x8f) return readVal; //Device not online + if (QTime::currentTime() >= timeOutTime) return QByteArray(); + } + } +} + +// ------------------------------------------------------------------------------------------------- +uint8_t FeatureSet::getFeatureIDFromDevice(FeatureCode fc) +{ + if (m_fdHIDDevice == -1) return 0x00; + + uint8_t fSetLSB = static_cast(static_cast(fc) >> 8); + uint8_t fSetMSB = static_cast(static_cast(fc)); + uint8_t featureIDReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray featureIDReqArr(reinterpret_cast(featureIDReq), sizeof(featureIDReq)); + ::write(m_fdHIDDevice, featureIDReqArr.data(), featureIDReqArr.length()); + + auto response = getResponseFromDevice(featureIDReqArr.mid(1, 3)); + if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; + uint8_t featureID = static_cast(response.at(4)); + + return featureID; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t featureSetID) +{ + if (m_fdHIDDevice == -1) return 0x00; + + // Get Number of features (except Root Feature) supported + uint8_t featureCountReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray featureCountReqArr(reinterpret_cast(featureCountReq), sizeof(featureCountReq)); + ::write(m_fdHIDDevice, featureCountReqArr.data(), featureCountReqArr.length()); + auto response = getResponseFromDevice(featureCountReqArr.mid(1, 3)); + if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; + uint8_t featureCount = static_cast(response.at(4)); + + return featureCount; +} + +// ------------------------------------------------------------------------------------------------- +QByteArray FeatureSet::getFirmwareVersionFromDevice() +{ + if (m_fdHIDDevice == -1) return 0x00; + + // To get firmware details: first get Feature ID corresponding to Firmware feature code + uint8_t fwID = getFeatureIDFromDevice(FeatureCode::FirmwareVersion); + if (!fwID) return QByteArray(); + + // Get the number of firmwares (Main HID++ application, BootLoader, or Hardware) now + uint8_t fwCountReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const QByteArray fwCountReqArr(reinterpret_cast(fwCountReq), sizeof(fwCountReq)); + ::write(m_fdHIDDevice, fwCountReqArr.data(), fwCountReqArr.length()); + auto response = getResponseFromDevice(fwCountReqArr.mid(1, 3)); + if (!response.length() || static_cast(response.at(2)) == 0x8f) return QByteArray(); + uint8_t fwCount = static_cast(response.at(4)); + + // The following info is not used currently; however, these commented lines are kept for future reference. + // uint8_t connectionMode = static_cast(response.at(10)); + // bool supportBluetooth = (connectionMode & 0x01); + // bool supportBluetoothLE = (connectionMode & 0x02); // true for Logitech Spotlight + // bool supportUsbReceiver = (connectionMode & 0x04); // true for Logitech Spotlight + // bool supportUsbWired = (connectionMode & 0x08); + // auto unitID = response.mid(5, 4); + // auto modelIDs = response.mid(11, 8); + // int count = 0; + // if (supportBluetooth) { auto btmodelID = modelIDs.mid(count, 2); count += 2;} + // if (supportBluetoothLE) { auto btlemodelID = modelIDs.mid(count, 2); count += 2;} + // if (supportUsbReceiver) { auto wpmodelID = modelIDs.mid(count, 2); count += 2;} + // if (supportUsbWired) { auto usbmodelID = modelIDs.mid(count, 2); count += 2;} + + + // Iteratively find out firmware version for all firmwares and get the firmware for main application + for (uint8_t i = 0x00; i < fwCount; i++) + { + uint8_t fwVerReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x1d, i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const QByteArray fwVerReqArr(reinterpret_cast(fwVerReq), sizeof(fwVerReq)); - ::write(m_fHIDDevice, fwVerReqArr.data(), fwVerReqArr.length()); - auto fwResponse1 = getResponse(fwVerReqArr.mid(1, 3)); - if (static_cast(fwResponse1.at(2)) == 0x8f) return; + ::write(m_fdHIDDevice, fwVerReqArr.data(), fwVerReqArr.length()); + auto fwResponse = getResponseFromDevice(fwVerReqArr.mid(1, 3)); + if (!fwResponse.length() || static_cast(fwResponse.at(2)) == 0x8f) return QByteArray(); + auto fwType = (fwResponse.at(4) & 0x0f); // 0 for main HID++ application, 1 for BootLoader, 2 for Hardware, 3-15 others + auto fwVersion = fwResponse.mid(5, 7); + // Currently we are not interested in these details; however, these commented lines are kept for future reference. + //auto firmwareName = fwVersion.mid(0, 3).data(); + //auto majorVesion = fwResponse.at(3); + //auto MinorVersion = fwResponse.at(4); + //auto build = fwResponse.mid(5); + if (fwType == 0) + { + logDebug(hid) << "Main application firmware Version:" << fwVersion.toHex(); + return fwVersion; + } + } + return QByteArray(); +} - uint8_t fwVer2Req[] = {0x11, 0x01, fwID, 0x1d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray fwVer2ReqArr(reinterpret_cast(fwVer2Req), sizeof(fwVer2Req)); - ::write(m_fHIDDevice, fwVer2ReqArr.data(), fwVer2ReqArr.length()); - auto fwResponse2 = getResponse(fwVer2ReqArr.mid(1, 3)); - if (static_cast(fwResponse2.at(2)) == 0x8f) return; - //TODO:: make sense of fwResponse1 and fwResponse2 +// ------------------------------------------------------------------------------------------------- +void FeatureSet::populateFeatureTable() +{ + if (m_fdHIDDevice == -1) return; + + // Get the firmware version + auto firmwareVersion = getFirmwareVersionFromDevice(); + if (!firmwareVersion.length()) return; - // TODO:: Read and write cache file - // if the firmware details match with cached file; then load the FeatureTable from file - // else read the entire feature table from the device + // TODO:: Read and write cache file (settings most probably) + // if the firmware details match with cached file; then load the FeatureTable from file + // else read the entire feature table from the device + QByteArray cacheFirmwareVersion; // currently a dummy variable for Firmware Version from cache file. + if (firmwareVersion == cacheFirmwareVersion) + { + // TODO: load the featureSet from the cache file + } else { // For reading feature table from device - // first get the Feature Index for Feature Set - uint8_t fSetLSB = static_cast(static_cast(FeatureCode::FeatureSet) >> 8); //0x00 - uint8_t fSetMSB = static_cast(static_cast(FeatureCode::FeatureSet)); //0x01 - uint8_t featureSetIDReq[] = {0x11, 0x01, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray featureSetIDReqArr(reinterpret_cast(featureSetIDReq), sizeof(featureSetIDReq)); - ::write(m_fHIDDevice, featureSetIDReqArr.data(), featureSetIDReqArr.length()); - response = getResponse(featureSetIDReqArr.mid(1, 3)); - if (static_cast(response.at(2)) == 0x8f) return; - uint8_t featureSetID = static_cast(response.at(4)); - - // Get Number of features (except Root Feature) supported - uint8_t featureCountReq[] = {0x11, 0x01, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray featureCountReqArr(reinterpret_cast(featureCountReq), sizeof(featureCountReq)); - ::write(m_fHIDDevice, featureCountReqArr.data(), featureCountReqArr.length()); - response = getResponse(featureCountReqArr.mid(1, 3)); - if (static_cast(response.at(2)) == 0x8f) return; - uint8_t featureCount = static_cast(response.at(4)); - - // Root feature is supported by all HID++ 2.0 device and has a featureID of 0 + // first get featureID for FeatureCode::FeatureSet + // then we can get the number of features supported by the device (except Root Feature) + uint8_t featureSetID = getFeatureIDFromDevice(FeatureCode::FeatureSet); + if (!featureSetID) return; + uint8_t featureCount = getFeatureCountFromDevice(featureSetID); + if (!featureCount) return; + + // Root feature is supported by all HID++ 2.0 device and has a featureID of 0 always. m_featureTable.insert({static_cast(FeatureCode::Root), 0x00}); - // Read Feature Code for other featureIds from device + // Read Feature Code for other featureIds from device. for (uint8_t featureId = 0x01; featureId <= featureCount; featureId++) { - const uint8_t data[] = {0x11, 0x01, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const uint8_t data[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const QByteArray dataArr(reinterpret_cast(data), sizeof(data)); - ::write(m_fHIDDevice, dataArr.data(), dataArr.length()); - response = getResponse(dataArr.mid(1, 3)); - if (static_cast(response.at(2)) == 0x8f) { + ::write(m_fdHIDDevice, dataArr.data(), dataArr.length()); + auto response = getResponseFromDevice(dataArr.mid(1, 3)); + if (!response.length() || static_cast(response.at(2)) == 0x8f) { m_featureTable.clear(); return; } @@ -86,9 +160,7 @@ void FeatureSet::populateFeatureTable(){ uint8_t featureType = static_cast(response.at(6)); auto softwareHidden = (featureType & (1<<6)); auto obsoleteFeature = (featureType & (1<<7)); - if (!(softwareHidden) && !(obsoleteFeature)) { - m_featureTable.insert({featureCode, featureId}); - } + if (!(softwareHidden) && !(obsoleteFeature)) m_featureTable.insert({featureCode, featureId}); } } } @@ -108,3 +180,18 @@ uint8_t FeatureSet::getFeatureID(FeatureCode fc) auto featurePair = m_featureTable.find(static_cast(fc)); return featurePair->second; } + +// ------------------------------------------------------------------------------------------------- +QByteArray HIDPP::shortToLongMsg(QByteArray shortMsg) +{ + bool isValidShortMsg = (shortMsg.at(0) == HIDPP_SHORT_MSG && shortMsg.length() == 7); + + if (isValidShortMsg) { + QByteArray longMsg; + longMsg.append(HIDPP_LONG_MSG); + longMsg.append(shortMsg.mid(1)); + QByteArray padding(20 - longMsg.length(), 0); + longMsg.append(padding); + return longMsg; + } else return shortMsg; +} diff --git a/src/hidpp.h b/src/hidpp.h index 426d2fa9..9b6fae86 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -6,6 +6,19 @@ #include #include +#define HIDPP_SHORT_MSG 0x10 +#define HIDPP_LONG_MSG 0x11 + +#define MSG_TO_USB_RECEIVER 0xff +#define MSG_TO_SPOTLIGHT 0x01 // Spotlight is first device on the receiver (bluetooth also uses this code) + +#define HIDPP_SHORT_GET_FEATURE 0x81 +#define HIDPP_SHORT_SET_FEATURE 0x80 + +#define HIDPP_SHORT_WIRELESS_NOTIFICATION_CODE 0x41 + + + // Feature Codes important for Logitech Spotlight enum class FeatureCode : uint16_t { Root = 0x0000, @@ -15,8 +28,6 @@ enum class FeatureCode : uint16_t { Reset = 0x0020, DFUControlSigned = 0x00c2, BatteryStatus = 0x1000, - BatteryVoltage = 0x1001, - BatteryUnified = 0x1004, PresenterControl = 0x1a00, Sensor3D = 0x1a01, ReprogramControlsV4 = 0x1b04, @@ -25,21 +36,29 @@ enum class FeatureCode : uint16_t { PointerSpeed = 0x2205, }; +namespace HIDPP { +QByteArray shortToLongMsg(QByteArray shortMsg); // used for bluetooth connections +} +// Class to get and store Set of supported features for a HID++ 2.0 device class FeatureSet { public: FeatureSet() {}; virtual ~FeatureSet() {} - void setHIDDeviceFileDescriptor(int fd) { m_fHIDDevice = fd; }; + void setHIDDeviceFileDescriptor(int fd) { m_fdHIDDevice = fd; } uint8_t getFeatureID(FeatureCode fc); bool supportFeatureCode(FeatureCode fc); auto getFeatureCount() { return m_featureTable.size(); } void populateFeatureTable(); - bool hasFeatureTable(){ return !(m_featureTable.empty()); } protected: + uint8_t getFeatureIDFromDevice(FeatureCode fc); + uint8_t getFeatureCountFromDevice(uint8_t featureSetID); + QByteArray getFirmwareVersionFromDevice(); + QByteArray getResponseFromDevice(QByteArray expectedBytes); + std::map m_featureTable; - int m_fHIDDevice=0; + int m_fdHIDDevice = -1; }; diff --git a/src/preferencesdlg.cc b/src/preferencesdlg.cc index a0fd3f36..73084783 100644 --- a/src/preferencesdlg.cc +++ b/src/preferencesdlg.cc @@ -103,7 +103,6 @@ PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, connect(overlayCheckBox, &QCheckBox::toggled, this, [settings](bool checked){ settings->setOverlayDisabled(!checked); - }); connect(settings, &Settings::overlayDisabledChanged, this, diff --git a/src/spotlight.cc b/src/spotlight.cc index 3783d334..5879aea3 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -139,18 +139,10 @@ int Spotlight::connectDevices() } else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); if(addHIDInputHandler(hidCon)) { - if (dc->deviceId().vendorId == 0x46d && hidCon->getFeatureSet()->getFeatureCount() == 0) { - //reconnect device to get the feature table - connect(hidCon.get(), &SubHidrawConnection::receivedPingResponse, - this, [this, hidCon](){ - removeDeviceConnection(hidCon->path()); - connectDevices();}); - } else { connect(hidCon.get(), &SubHidrawConnection::receivedBatteryInfo, dc.get(), &DeviceConnection::setBatteryInfo); connect(hidCon.get(), &SubHidrawConnection::receivedPingResponse, dc.get(), [this, dc](){emit deviceActivated(dc->deviceId(), dc->deviceName());}); - } return hidCon; } if(addHIDInputHandler(hidCon)) return hidCon; @@ -330,29 +322,28 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) - if (!(readVal.at(0) == 0x10 || readVal.at(0) == 0x11)) { + if (!(readVal.at(0) == HIDPP_SHORT_MSG || readVal.at(0) == HIDPP_LONG_MSG)) { return; } logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - if (readVal.at(0) == 0x10) // Logitech HIDPP SHORT message: 7 byte long + if (readVal.at(0) == HIDPP_SHORT_MSG) // Logitech HIDPP SHORT message: 7 byte long { - if (readVal.at(2) == 0x41 && !!(readVal.at(3) & 0x04)) { // wireless notification from USB dongle - if (readVal.at(4) & (1<<6)) { // connection between USB dongle and spotlight device broke + if (readVal.at(2) == HIDPP_SHORT_WIRELESS_NOTIFICATION_CODE) { // wireless notification from USB dongle + auto connection_status = readVal.at(4) & (1<<6); // should be zero for successful connection + if (connection_status) { // connection between USB dongle and spotlight device broke connection.setHIDProtocol(-1); } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. - // currently it is off as I observed that device send two online packet - // one with 0x10 and other with 0x11. Currently initsubDevice is triggered - // on 0x11 packet. - //connection.initSubDevice(); + if (!connection.isOnline()) connection.initSubDevice(); } } } - if (readVal.at(0) == 0x11) // Logitech HIDPP LONG message: 20 byte long + if (readVal.at(0) == HIDPP_LONG_MSG) // Logitech HIDPP LONG message: 20 byte long { - if (readVal.at(2) == 0x00) { + auto rootID = connection.getFeatureSet()->getFeatureID(FeatureCode::Root); + if (readVal.at(2) == rootID) { if (readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) { // response to ping auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; connection.setHIDProtocol(protocolVer); @@ -360,18 +351,23 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } } - if (readVal.at(2) == 0x04) { // Logitech spotlight presenter unit got online. - connection.initSubDevice(); + auto wirelessNotificationID = connection.getFeatureSet()->getFeatureID(FeatureCode::WirelessDeviceStatus); + if (wirelessNotificationID && readVal.at(2) == wirelessNotificationID) { // Logitech spotlight presenter unit got online. + if (!connection.isOnline()) connection.initSubDevice(); } - if (readVal.at(2) == 0x06 && readVal.at(3) == 0x0d) { // Battery information packet + // Battery packet processing: Device responded to BatteryStatus (0x1000) packet + auto batteryID = connection.getFeatureSet()->getFeatureID(FeatureCode::BatteryStatus); + if (batteryID && readVal.at(2) == batteryID && readVal.at(3) == 0x0d) { // Battery information packet QByteArray batteryData(readVal.mid(4, 3)); emit connection.receivedBatteryInfo(batteryData); } // TODO: Process other packets - if (readVal.at(2) == 0x09 && readVal.at(3) == 0x1d) { + // Vibration response check + const uint8_t pcID = connection.getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); + if (pcID && readVal.at(2) == pcID && readVal.at(3) == 0x1d) { logDebug(hid) << "Device acknowledged a vibration event."; } } From 4712b6d7e892ef7b8f761687b67a7038a2639ba8 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 23 Jul 2021 14:54:40 +0200 Subject: [PATCH 029/110] Fix ci-build for unused return values and refactor some hidpp code. --- src/device.cc | 9 +-- src/deviceswidget.cc | 3 + src/deviceswidget.h | 11 ++-- src/hidpp.cc | 152 ++++++++++++++++++++++++++++++------------- src/hidpp.h | 15 ++--- 5 files changed, 127 insertions(+), 63 deletions(-) diff --git a/src/device.cc b/src/device.cc index 615d4cde..942f21bc 100644 --- a/src/device.cc +++ b/src/device.cc @@ -25,10 +25,11 @@ namespace { // ------------------------------------------------------------------------------------------------- DeviceConnection::DeviceConnection(const DeviceId& id, const QString& name, std::shared_ptr vdev) - : m_deviceId(id), - m_deviceName(name), - m_inputMapper(std::make_shared(std::move(vdev))){ - m_featureSet = std::make_shared(FeatureSet()); + : m_deviceId(id) + , m_deviceName(name) + , m_inputMapper(std::make_shared(std::move(vdev))) + , m_featureSet(std::make_shared()) +{ } // ------------------------------------------------------------------------------------------------- diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 2a7add07..9a9758da 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -17,6 +17,8 @@ #include #include #include +#include +#include DECLARE_LOGGING_CATEGORY(preferences) @@ -34,6 +36,7 @@ namespace { // ------------------------------------------------------------------------------------------------- DevicesWidget::DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent) : QWidget(parent) + , m_updateDeviceDetailsTimer(new QTimer(this)) { createDeviceComboBox(spotlight); diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 8be1717e..b1dab1ef 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -3,9 +3,6 @@ #include #include -#include -#include -#include struct DeviceId; class InputMapper; @@ -14,6 +11,9 @@ class Settings; class Spotlight; class VibrationSettingsWidget; +class QTimer; +class QTextEdit; + // ------------------------------------------------------------------------------------------------- class DevicesWidget : public QWidget { @@ -38,8 +38,11 @@ class DevicesWidget : public QWidget QComboBox* m_devicesCombo = nullptr; QWidget* m_timerTabWidget = nullptr; QWidget* m_deviceDetailsTabWidget = nullptr; + + // TODO Put into separate DeviceDetailsWidget QTextEdit* m_deviceDetailsTextEdit = nullptr; - QTimer* m_updateDeviceDetailsTimer = new QTimer(this); + QTimer* m_updateDeviceDetailsTimer = nullptr; + VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; QPointer m_inputMapper; }; diff --git a/src/hidpp.cc b/src/hidpp.cc index e0f6bea2..3d43fe15 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -9,7 +9,19 @@ DECLARE_LOGGING_CATEGORY(hid) // ------------------------------------------------------------------------------------------------- -QByteArray FeatureSet::getResponseFromDevice(QByteArray expectedBytes) +namespace { + using HidppMsg = uint8_t[]; + + template + QByteArray make_QByteArray(const C(&a)[N]) { + return {reinterpret_cast(a),N}; + } + + class Hid_ : public QObject {}; // for i18n and logging +} + +// ------------------------------------------------------------------------------------------------- +QByteArray FeatureSet::getResponseFromDevice(const QByteArray& expectedBytes) { if (m_fdHIDDevice == -1) return QByteArray(); @@ -31,13 +43,22 @@ uint8_t FeatureSet::getFeatureIDFromDevice(FeatureCode fc) { if (m_fdHIDDevice == -1) return 0x00; - uint8_t fSetLSB = static_cast(static_cast(fc) >> 8); - uint8_t fSetMSB = static_cast(static_cast(fc)); - uint8_t featureIDReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray featureIDReqArr(reinterpret_cast(featureIDReq), sizeof(featureIDReq)); - ::write(m_fdHIDDevice, featureIDReqArr.data(), featureIDReqArr.length()); + const uint8_t fSetLSB = static_cast(static_cast(fc) >> 8); + const uint8_t fSetMSB = static_cast(static_cast(fc)); + + const auto featureReqMessage = make_QByteArray(HidppMsg{ + HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + + const auto res = ::write(m_fdHIDDevice, featureReqMessage.data(), featureReqMessage.size()); + if (res != featureReqMessage.size()) + { + logDebug(hid) << Hid_::tr("Failed to write feature request message to device."); + return 0x00; + } - auto response = getResponseFromDevice(featureIDReqArr.mid(1, 3)); + const auto response = getResponseFromDevice(featureReqMessage.mid(1, 3)); if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; uint8_t featureID = static_cast(response.at(4)); @@ -50,10 +71,19 @@ uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t featureSetID) if (m_fdHIDDevice == -1) return 0x00; // Get Number of features (except Root Feature) supported - uint8_t featureCountReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray featureCountReqArr(reinterpret_cast(featureCountReq), sizeof(featureCountReq)); - ::write(m_fdHIDDevice, featureCountReqArr.data(), featureCountReqArr.length()); - auto response = getResponseFromDevice(featureCountReqArr.mid(1, 3)); + const auto featureCountReqMessage = make_QByteArray(HidppMsg{ + HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + + const auto res = ::write(m_fdHIDDevice, featureCountReqMessage.data(), featureCountReqMessage.size()); + if (res != featureCountReqMessage.size()) + { + logDebug(hid) << Hid_::tr("Failed to write feature count request message to device."); + return 0x00; + } + + const auto response = getResponseFromDevice(featureCountReqMessage.mid(1, 3)); if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; uint8_t featureCount = static_cast(response.at(4)); @@ -70,12 +100,21 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() if (!fwID) return QByteArray(); // Get the number of firmwares (Main HID++ application, BootLoader, or Hardware) now - uint8_t fwCountReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray fwCountReqArr(reinterpret_cast(fwCountReq), sizeof(fwCountReq)); - ::write(m_fdHIDDevice, fwCountReqArr.data(), fwCountReqArr.length()); - auto response = getResponseFromDevice(fwCountReqArr.mid(1, 3)); - if (!response.length() || static_cast(response.at(2)) == 0x8f) return QByteArray(); - uint8_t fwCount = static_cast(response.at(4)); + const auto fwCountReqMessage = make_QByteArray(HidppMsg{ + HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + + const auto res = ::write(m_fdHIDDevice, fwCountReqMessage.data(), fwCountReqMessage.size()); + if (res != fwCountReqMessage.size()) + { + logDebug(hid) << Hid_::tr("Failed to write firmware count request message to device."); + return 0x00; + } + + const auto response = getResponseFromDevice(fwCountReqMessage.mid(1, 3)); + if (!response.length() || static_cast(response.at(2)) == 0x8f) return QByteArray(); + const uint8_t fwCount = static_cast(response.at(4)); // The following info is not used currently; however, these commented lines are kept for future reference. // uint8_t connectionMode = static_cast(response.at(10)); @@ -92,16 +131,25 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() // if (supportUsbWired) { auto usbmodelID = modelIDs.mid(count, 2); count += 2;} - // Iteratively find out firmware version for all firmwares and get the firmware for main application + // Iteratively find out firmware versions for all firmwares and get the firmware for main application for (uint8_t i = 0x00; i < fwCount; i++) { - uint8_t fwVerReq[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x1d, i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray fwVerReqArr(reinterpret_cast(fwVerReq), sizeof(fwVerReq)); - ::write(m_fdHIDDevice, fwVerReqArr.data(), fwVerReqArr.length()); - auto fwResponse = getResponseFromDevice(fwVerReqArr.mid(1, 3)); + const auto fwVerReqMessage = make_QByteArray(HidppMsg{ + HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x1d, i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + + const auto res = ::write(m_fdHIDDevice, fwVerReqMessage.data(), fwVerReqMessage.length()); + if (res != fwCountReqMessage.size()) + { + logDebug(hid) << Hid_::tr("Failed to write firmware request message to device (%1).") + .arg(int(i)); + return 0x00; + } + const auto fwResponse = getResponseFromDevice(fwVerReqMessage.mid(1, 3)); if (!fwResponse.length() || static_cast(fwResponse.at(2)) == 0x8f) return QByteArray(); - auto fwType = (fwResponse.at(4) & 0x0f); // 0 for main HID++ application, 1 for BootLoader, 2 for Hardware, 3-15 others - auto fwVersion = fwResponse.mid(5, 7); + const auto fwType = (fwResponse.at(4) & 0x0f); // 0 for main HID++ application, 1 for BootLoader, 2 for Hardware, 3-15 others + const auto fwVersion = fwResponse.mid(5, 7); // Currently we are not interested in these details; however, these commented lines are kept for future reference. //auto firmwareName = fwVersion.mid(0, 3).data(); //auto majorVesion = fwResponse.at(3); @@ -122,7 +170,7 @@ void FeatureSet::populateFeatureTable() if (m_fdHIDDevice == -1) return; // Get the firmware version - auto firmwareVersion = getFirmwareVersionFromDevice(); + const auto firmwareVersion = getFirmwareVersionFromDevice(); if (!firmwareVersion.length()) return; // TODO:: Read and write cache file (settings most probably) @@ -133,33 +181,42 @@ void FeatureSet::populateFeatureTable() if (firmwareVersion == cacheFirmwareVersion) { // TODO: load the featureSet from the cache file - - } else { + } + else + { // For reading feature table from device // first get featureID for FeatureCode::FeatureSet // then we can get the number of features supported by the device (except Root Feature) - uint8_t featureSetID = getFeatureIDFromDevice(FeatureCode::FeatureSet); + const uint8_t featureSetID = getFeatureIDFromDevice(FeatureCode::FeatureSet); if (!featureSetID) return; - uint8_t featureCount = getFeatureCountFromDevice(featureSetID); + const uint8_t featureCount = getFeatureCountFromDevice(featureSetID); if (!featureCount) return; // Root feature is supported by all HID++ 2.0 device and has a featureID of 0 always. m_featureTable.insert({static_cast(FeatureCode::Root), 0x00}); // Read Feature Code for other featureIds from device. - for (uint8_t featureId = 0x01; featureId <= featureCount; featureId++) { - const uint8_t data[] = {HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - const QByteArray dataArr(reinterpret_cast(data), sizeof(data)); - ::write(m_fdHIDDevice, dataArr.data(), dataArr.length()); - auto response = getResponseFromDevice(dataArr.mid(1, 3)); + for (uint8_t featureId = 0x01; featureId <= featureCount; ++featureId) + { + const auto featureCodeReqMsg = make_QByteArray(HidppMsg{ + HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }); + const auto res = ::write(m_fdHIDDevice, featureCodeReqMsg.data(), featureCodeReqMsg.size()); + if (res != featureCodeReqMsg.size()) { + logDebug(hid) << Hid_::tr("Failed to write feature code request message to device."); + return; + } + + const auto response = getResponseFromDevice(featureCodeReqMsg.mid(1, 3)); if (!response.length() || static_cast(response.at(2)) == 0x8f) { m_featureTable.clear(); return; } - uint16_t featureCode = (static_cast(response.at(4)) << 8) | static_cast(response.at(5)); - uint8_t featureType = static_cast(response.at(6)); - auto softwareHidden = (featureType & (1<<6)); - auto obsoleteFeature = (featureType & (1<<7)); + const uint16_t featureCode = (static_cast(response.at(4)) << 8) | static_cast(response.at(5)); + const uint8_t featureType = static_cast(response.at(6)); + const auto softwareHidden = (featureType & (1<<6)); + const auto obsoleteFeature = (featureType & (1<<7)); if (!(softwareHidden) && !(obsoleteFeature)) m_featureTable.insert({featureCode, featureId}); } } @@ -168,7 +225,7 @@ void FeatureSet::populateFeatureTable() // ------------------------------------------------------------------------------------------------- bool FeatureSet::supportFeatureCode(FeatureCode fc) { - auto featurePair = m_featureTable.find(static_cast(fc)); + const auto featurePair = m_featureTable.find(static_cast(fc)); return (featurePair != m_featureTable.end()); } @@ -177,21 +234,24 @@ uint8_t FeatureSet::getFeatureID(FeatureCode fc) { if (!supportFeatureCode(fc)) return 0x00; - auto featurePair = m_featureTable.find(static_cast(fc)); + const auto featurePair = m_featureTable.find(static_cast(fc)); return featurePair->second; } // ------------------------------------------------------------------------------------------------- -QByteArray HIDPP::shortToLongMsg(QByteArray shortMsg) +QByteArray HIDPP::shortToLongMsg(const QByteArray& shortMsg) { - bool isValidShortMsg = (shortMsg.at(0) == HIDPP_SHORT_MSG && shortMsg.length() == 7); + const bool isValidShortMsg = (shortMsg.at(0) == HIDPP_SHORT_MSG && shortMsg.length() == 7); - if (isValidShortMsg) { + if (isValidShortMsg) + { QByteArray longMsg; + longMsg.reserve(20); longMsg.append(HIDPP_LONG_MSG); longMsg.append(shortMsg.mid(1)); - QByteArray padding(20 - longMsg.length(), 0); - longMsg.append(padding); + longMsg.append(20 - longMsg.length(), 0); return longMsg; - } else return shortMsg; + } + + return shortMsg; } diff --git a/src/hidpp.h b/src/hidpp.h index 9b6fae86..f9469575 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -1,10 +1,9 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once -#include +#include -#include -#include +#include #define HIDPP_SHORT_MSG 0x10 #define HIDPP_LONG_MSG 0x11 @@ -37,27 +36,25 @@ enum class FeatureCode : uint16_t { }; namespace HIDPP { -QByteArray shortToLongMsg(QByteArray shortMsg); // used for bluetooth connections + /// Used for Bluetooth connections + QByteArray shortToLongMsg(const QByteArray& shortMsg); } // Class to get and store Set of supported features for a HID++ 2.0 device class FeatureSet { public: - FeatureSet() {}; - virtual ~FeatureSet() {} - void setHIDDeviceFileDescriptor(int fd) { m_fdHIDDevice = fd; } uint8_t getFeatureID(FeatureCode fc); bool supportFeatureCode(FeatureCode fc); auto getFeatureCount() { return m_featureTable.size(); } void populateFeatureTable(); -protected: +private: uint8_t getFeatureIDFromDevice(FeatureCode fc); uint8_t getFeatureCountFromDevice(uint8_t featureSetID); QByteArray getFirmwareVersionFromDevice(); - QByteArray getResponseFromDevice(QByteArray expectedBytes); + QByteArray getResponseFromDevice(const QByteArray& expectedBytes); std::map m_featureTable; int m_fdHIDDevice = -1; From 9f0bf076d57849ff9a98ee2991ac0269dba6cbd8 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 31 Jul 2021 17:16:10 +0200 Subject: [PATCH 030/110] Add asynchronous header and refactor code. --- src/asynchronous.h | 125 +++++++++++++++++++++ src/device-vibration.cc | 10 +- src/device.cc | 243 +++++++++++++++++++++++----------------- src/device.h | 34 ++++-- src/devicescan.h | 1 - src/deviceswidget.cc | 235 +++++++++++++++++++++----------------- src/deviceswidget.h | 33 +++++- src/projecteurapp.cc | 2 - src/projecteurapp.h | 1 - src/runguard.h | 1 - src/spotlight.cc | 30 +++-- 11 files changed, 475 insertions(+), 240 deletions(-) create mode 100644 src/asynchronous.h diff --git a/src/asynchronous.h b/src/asynchronous.h new file mode 100644 index 00000000..a3a698c0 --- /dev/null +++ b/src/asynchronous.h @@ -0,0 +1,125 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +#pragma once + +#include +#include + +#include +#include + +namespace async { + +// Invoke a (lambda) function for context QObject with queued connection. +template +void invoke(QObject* context, F function) { + QMetaObject::invokeMethod(context, std::move(function), Qt::QueuedConnection); +} + +// --- Helpers to deduce std::function type from a lambda. +template +struct remove_member; + +template +struct remove_member { + using type = T; +}; + +template +struct remove_member { + using type = R(Args...); +}; + +/// Create a safe function object, guaranteed to be invoked in the context of +/// the given QObject context. +template +std::function makeSafeCallback(QObject* context, + std::function f, + bool forceQueued = true) +{ + QPointer ctxPtr(context); + std::function res = + [ctxPtr, forceQueued, func = std::move(f)](Args... args) { + // Check if context object is still valid + if (ctxPtr.isNull()) { + return; + } + + QMetaObject::invokeMethod(ctxPtr, + std::bind(std::move(func), std::forward(args)...), + forceQueued ? Qt::QueuedConnection : Qt::AutoConnection); + // Note: if forceQueued is false and current thread is the same as + // the context thread -> execute directly + }; + return res; +} + +template +auto makeSafeCallback(QObject* context, F f, bool forceQueued = true) { + using ft = decltype(&F::operator()); + std::function::type> func = std::move(f); + return async::makeSafeCallback(context, std::move(func), forceQueued); +} + +/// Deriving from this class will makeSafeCallback and postSelf methods for QObject based +/// classes available. +/// +/// Example: +/// @code +/// class MyClass : public QObject, public async::Async { +/// Q_OBJECT +/// // ... implementation.. +/// } +/// @endcode +template +class Async +{ +protected: + /// Returns a function object that is guaranteed to be invoked in the own thread context. + template + auto makeSafeCallback(F f) { + return async::makeSafeCallback(static_cast(this), std::move(f)); + } + + /// Post a function to the own event loop. + template + void postSelf(F function) { + invoke(static_cast(this), std::move(function)); + } + +public: + /// Post a task to the object's event loop. + template + void postTask(Task task) { + postSelf(std::move(task)); + } + + template + static constexpr bool is_void_return_v = std::is_same, void>::value; + + /// Post a task with no return value and provide a callback. + template + typename std::enable_if_t> + postCallback(Task task, Callback callback) + { + auto wrapper = [task = std::move(task), callback = std::move(callback)]() mutable + { + task(); + callback(); + }; + postSelf(std::move(wrapper)); + } + + /// Post a task with return value and a callback that takes the return value + /// as an argument. + template + typename std::enable_if_t> + postCallback(Task task, Callback callback) + { + auto wrapper = [task = std::move(task), callback = std::move(callback)]() mutable { + callback(task()); + }; + postSelf(std::move(wrapper)); + } +}; + +} // end namespace async diff --git a/src/device-vibration.cc b/src/device-vibration.cc index 8329344f..576613c5 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -2,6 +2,7 @@ #include "device-vibration.h" #include "device.h" +#include "hidpp.h" #include "iconwidgets.h" #include "logging.h" @@ -18,7 +19,6 @@ #include #include -#include // ------------------------------------------------------------------------------------------------- namespace { @@ -412,7 +412,7 @@ void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc) void VibrationSettingsWidget::sendVibrateCommand() { if (!m_subDeviceConnection) return; - if ((m_subDeviceConnection->flags() & DeviceFlag::Vibrate) != DeviceFlag::Vibrate) return; + if (!m_subDeviceConnection->hasFlags(DeviceFlag::Vibrate)) return; if (!m_subDeviceConnection->isConnected()) return; // TODO generalize features and protocol for proprietary device features like vibration @@ -423,10 +423,12 @@ void VibrationSettingsWidget::sendVibrateCommand() // controlID len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; + const uint8_t pcID = m_subDeviceConnection->getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); + if (pcID == 0x00) return; + const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); - const uint8_t pcID = m_subDeviceConnection->getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); const uint8_t vibrateCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, pcID, 0x1d, vlen, 0xe8, vint}; - if (pcID) m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); + m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); } diff --git a/src/device.cc b/src/device.cc index 942f21bc..4cea9a26 100644 --- a/src/device.cc +++ b/src/device.cc @@ -3,6 +3,7 @@ #include "deviceinput.h" #include "devicescan.h" +#include "hidpp.h" #include "logging.h" #include @@ -20,6 +21,12 @@ namespace { static const auto registeredComparator_ = QMetaType::registerComparators(); const auto hexId = logging::hexId; + + // ----------------------------------------------------------------------------------------------- + bool deviceHasHidppSupport(const DeviceId& id) { + // HID++ only for Logitech devices + return id.vendorId == 0x046d; + } } // ------------------------------------------------------------------------------------------------- @@ -48,6 +55,10 @@ void DeviceConnection::addSubDevice(std::shared_ptr sdc) if (!sdc) return; const auto path = sdc->path(); + connect(&*sdc, &SubDeviceConnection::flagsChanged, this, [this, path](){ + emit subDeviceFlagsChanged(m_deviceId, path); + }); + m_subDeviceConnections[path] = std::move(sdc); emit subDeviceConnected(m_deviceId, path); } @@ -72,11 +83,12 @@ bool DeviceConnection::removeSubDevice(const QString& path) // ------------------------------------------------------------------------------------------------- void DeviceConnection::queryBatteryStatus() { - if (subDeviceCount() > 0) { - for (const auto& sd: subDevices()) { - if (sd.second->type() == ConnectionType::Hidraw && sd.second->mode() == ConnectionMode::ReadWrite) { - sd.second->queryBatteryStatus(); - } + for (const auto& sd: subDevices()) + { + if (sd.second->type() == ConnectionType::Hidraw && sd.second->mode() == ConnectionMode::ReadWrite) + { + const auto hidrawConn = std::static_pointer_cast(sd.second); + hidrawConn->queryBatteryStatus(); } } } @@ -100,6 +112,22 @@ SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType typ // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; +// ------------------------------------------------------------------------------------------------- +DeviceFlags SubDeviceConnection::setFlags(DeviceFlags f, bool set) +{ + const auto previousFlags = flags(); + if (set) { + m_details.deviceFlags |= f; + } else { + m_details.deviceFlags &= ~f; + } + + if (m_details.deviceFlags != previousFlags) { + emit flagsChanged(m_details.deviceFlags); + } + return m_details.deviceFlags; +} + // ------------------------------------------------------------------------------------------------- bool SubDeviceConnection::isConnected() const { if (type() == ConnectionType::Event) @@ -116,19 +144,19 @@ void SubDeviceConnection::disconnect() { } // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::disable() { - if (m_readNotifier) m_readNotifier->setEnabled(false); - if (m_writeNotifier) m_writeNotifier->setEnabled(false); +void SubDeviceConnection::setNotifiersEnabled(bool enabled) { + setReadNotifierEnabled(enabled); + setWriteNotifierEnabled(enabled); } // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::disableWrite() { - if (m_writeNotifier) m_writeNotifier->setEnabled(false); +void SubDeviceConnection::setReadNotifierEnabled(bool enabled) { + if (m_readNotifier) m_readNotifier->setEnabled(enabled); } // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::enableWrite() { - if (m_writeNotifier) m_writeNotifier->setEnabled(true); +void SubDeviceConnection::setWriteNotifierEnabled(bool enabled) { + if (m_writeNotifier) m_writeNotifier->setEnabled(enabled); } // ------------------------------------------------------------------------------------------------- @@ -284,14 +312,21 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca auto connection = std::make_shared(Token{}, sd.deviceFile); connection->m_deviceID = dc.deviceId(); + + // TODO feature set needs to be a member of a sub hidraw connection connection->m_featureSet = dc.getFeatureSet(); connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); + // --- fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; } + if (deviceHasHidppSupport(dc.deviceId())) { + connection->m_details.deviceFlags |= DeviceFlag::Hidpp; + } + // Create read and write socket notifiers connection->m_readNotifier = std::make_unique(devfd, QSocketNotifier::Read); QSocketNotifier* const readNotifier = connection->m_readNotifier.get(); @@ -302,20 +337,34 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca connection->m_writeNotifier = std::make_unique(devfd, QSocketNotifier::Write); QSocketNotifier* const writeNotifier = connection->m_writeNotifier.get(); + writeNotifier->setEnabled(false); // Disable write notifier by default // Auto clean up and close descriptor on destruction of notifier connect(writeNotifier, &QSocketNotifier::destroyed, [writeNotifier]() { ::close(static_cast(writeNotifier->socket())); }); connection->m_details.phys = sd.phys; - connection->initSubDevice(); return connection; } +// ------------------------------------------------------------------------------------------------- +void SubHidrawConnection::queryBatteryStatus() +{ + if (hasFlags(DeviceFlag::ReportBattery)) + { + const uint8_t batteryFeatureID = m_featureSet->getFeatureID(FeatureCode::BatteryStatus); + if (batteryFeatureID) + { + const uint8_t batteryCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; + sendData(batteryCmd, sizeof(batteryCmd), false); + } + } +} + // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::pingSubDevice() { - uint8_t rootID = 0x00; // root ID is always 0x00 in any logitech device + constexpr uint8_t rootID = 0x00; // root ID is always 0x00 in any logitech device const uint8_t pingCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, rootID, 0x1d, 0x00, 0x00, 0x5d}; sendData(pingCmd, sizeof(pingCmd), false); } @@ -331,99 +380,86 @@ void SubDeviceConnection::setHIDProtocol(float version) { } // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::initSubDevice() +void SubHidrawConnection::initialize() { - int msgCount = 0, delay_ms = 20; - if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite) + // Currently only HID++ devices need additional initializing + if (!hasFlags(DeviceFlag::Hidpp)) return; + + constexpr int delay_ms = 20; + int msgCount = 0; + // Reset device: get rid of any device configuration by other programs ------- + if (m_deviceID.busType == BusType::Usb) { - if (m_deviceID.vendorId == 0x46d) // Only check FeatureSet for Logitech devices - { - // Reset device: get rid of any device configuration by other programs ------- - if (m_deviceID.busType == BusType::Usb) - { - // Reset USB dongle - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_GET_FEATURE, 0x00, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - - // Turn off software bit and keep the wireless notification bit on - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x01, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - - // Initialize USB dongle - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x02, 0x02, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x09, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - } - - // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from logitech device - disable(); - if (m_featureSet->getFeatureCount() == 0) m_featureSet->populateFeatureTable(); - if (m_featureSet->getFeatureCount()) { - logDebug(hid) << "Loaded" << m_featureSet->getFeatureCount() << "features for" << path(); - if (m_featureSet->supportFeatureCode(FeatureCode::PresenterControl)) { - m_details.deviceFlags |= DeviceFlag::Vibrate; - logDebug(hid) << "SubDevice" << path() << "reported Vibration capabilities."; - } - if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus)) { - m_details.deviceFlags |= DeviceFlag::ReportBattery; - logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; - } - } else { - logWarn(hid) << "Loading FeatureSet for" << path() << "failed. Device might be inactive."; - logInfo(hid) << "Press any button on device to activate it."; - } - if (m_readNotifier) m_readNotifier->setEnabled(true); - disableWrite(); - - // Reset spotlight device - if (m_featureSet->getFeatureCount()) { - const auto resetID = m_featureSet->getFeatureID(FeatureCode::Reset); - if (resetID) { - QTimer::singleShot(delay_ms*msgCount, this, [this, resetID](){ - const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, resetID, 0x1d, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); - msgCount++; - } - } - // Device Resetting complete ------------------------------------------------- - - if (m_deviceID.busType == BusType::Usb) { - // Ping spotlight device for checking if is online - // the response will have the version for HID++ protocol. - QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); - msgCount++; - } else if (m_deviceID.busType == BusType::Bluetooth) { - // Bluetooth connection mean HID++ v2.0+. - // Setting version to 6.4: same as USB connection. - setHIDProtocol(6.4); - } - // Add other configuration to enable features in device - // like enabling on Next and back button on hold functionality. - } + // Reset USB dongle + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_GET_FEATURE, 0x00, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + // Turn off software bit and keep the wireless notification bit on + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x01, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + // Initialize USB dongle + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x02, 0x02, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + + QTimer::singleShot(delay_ms*msgCount, this, [this](){ + constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x09, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; } - // No initialization for Event SubDevice -} -// ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::queryBatteryStatus() -{ - if (!!(flags() & DeviceFlag::ReportBattery)) { - const uint8_t batteryFeatureID = m_featureSet->getFeatureID(FeatureCode::BatteryStatus); - if (batteryFeatureID) { - const uint8_t batteryCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; - sendData(batteryCmd, sizeof(batteryCmd), false); + DeviceFlags featureFlags = DeviceFlag::NoFlags; + // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from logitech device + setNotifiersEnabled(false); + if (m_featureSet->getFeatureCount() == 0) m_featureSet->populateFeatureTable(); + if (m_featureSet->getFeatureCount()) { + logDebug(hid) << "Loaded" << m_featureSet->getFeatureCount() << "features for" << path(); + if (m_featureSet->supportFeatureCode(FeatureCode::PresenterControl)) { + featureFlags |= DeviceFlag::Vibrate; + logDebug(hid) << "SubDevice" << path() << "reported Vibration capabilities."; + } + if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus)) { + featureFlags |= DeviceFlag::ReportBattery; + logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; } + } else { + logWarn(hid) << "Loading FeatureSet for" << path() << "failed. Device might be inactive."; + logInfo(hid) << "Press any button on device to activate it."; } + setReadNotifierEnabled(true); + + // Reset spotlight device + if (m_featureSet->getFeatureCount()) { + const auto resetID = m_featureSet->getFeatureID(FeatureCode::Reset); + if (resetID) { + QTimer::singleShot(delay_ms*msgCount, this, [this, resetID](){ + const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, resetID, 0x1d, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data), false);}); + msgCount++; + } + } + // Device Resetting complete ------------------------------------------------- + + if (m_deviceID.busType == BusType::Usb) { + // Ping spotlight device for checking if is online + // the response will have the version for HID++ protocol. + QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); + msgCount++; + } else if (m_deviceID.busType == BusType::Bluetooth) { + // Bluetooth connection mean HID++ v2.0+. + // Setting version to 6.4: same as USB connection. + setHIDProtocol(6.4); + } + + setFlags(featureFlags, true); + // Add other configuration to enable features in device + // like enabling on Next and back button on hold functionality. } // ------------------------------------------------------------------------------------------------- @@ -464,11 +500,10 @@ ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDevi } if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite - && m_writeNotifier && isValidMsg) { - enableWrite(); + && m_writeNotifier && isValidMsg) + { const auto notifier = socketWriteNotifier(); res = ::write(notifier->socket(), _hidppMsg.data(), _hidppMsg.length()); - disableWrite(); if (res == _hidppMsg.length()) { logDebug(hid) << "Write" << _hidppMsg.toHex() << "to" << path(); diff --git a/src/device.h b/src/device.h index ec743985..a20d2c6b 100644 --- a/src/device.h +++ b/src/device.h @@ -1,10 +1,11 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once +#include "asynchronous.h" #include "enum-helper.h" -#include "hidpp.h" #include +#include #include @@ -96,6 +97,7 @@ public slots: signals: void subDeviceConnected(const DeviceId& id, const QString& path); void subDeviceDisconnected(const DeviceId& id, const QString& path); + void subDeviceFlagsChanged(const DeviceId& id, const QString& path); protected: using DevicePath = QString; @@ -118,7 +120,8 @@ enum class DeviceFlag : uint32_t { RelativeEvents = 1 << 3, KeyEvents = 1 << 4, - Vibrate = 1 << 16, + Hidpp = 1 << 15, ///< Device supports hidpp requests + Vibrate = 1 << 16, ///< Device supports vibrate commands ReportBattery = 1 << 17, }; ENUM(DeviceFlag, DeviceFlags) @@ -154,7 +157,7 @@ struct InputBuffer { }; // ------------------------------------------------------------------------------------------------- -class SubDeviceConnection : public QObject +class SubDeviceConnection : public QObject, public async::Async { Q_OBJECT public: @@ -162,18 +165,17 @@ class SubDeviceConnection : public QObject bool isConnected() const; void disconnect(); // destroys socket notifier and close file handle - void disable(); // disable receiving/sending data - void disableWrite(); // disable sending data - void enableWrite(); // enable sending data + void setNotifiersEnabled(bool enabled); // enable/disable read and write socket notifiers + void setReadNotifierEnabled(bool enabled); // disable/enable read socket notifier + void setWriteNotifierEnabled(bool enabled); // disable/enable write socket notifier // HID++ specific functions const auto& getFeatureSet () const { return m_featureSet; }; - void initSubDevice(); void pingSubDevice(); - bool isOnline() { return (m_details.hidProtocolVer > 0); }; + bool isOnline() const { return (m_details.hidProtocolVer > 0); }; void setHIDProtocol(float version); - float getHIDProtocol() { return m_details.hidProtocolVer; }; - void queryBatteryStatus(); + float getHIDProtocol() const { return m_details.hidProtocolVer; }; + ssize_t sendData(const QByteArray& hidppMsg, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection @@ -184,18 +186,24 @@ class SubDeviceConnection : public QObject const auto& phys() const { return m_details.phys; }; const auto& path() const { return m_details.devicePath; }; + inline bool hasFlags(DeviceFlags f) const { return ((flags() & f) == f); } + const std::shared_ptr& inputMapper() const; QSocketNotifier* socketReadNotifier(); // Read notifier for Hidraw and Event connections for receiving data from device QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device +signals: + void flagsChanged(DeviceFlags f); + protected: SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode); + DeviceFlags setFlags(DeviceFlags f, bool set = true); SubDeviceConnectionDetails m_details; std::shared_ptr m_inputMapper; // shared input mapper from parent device. std::unique_ptr m_readNotifier; std::unique_ptr m_writeNotifier; // only useful for Hidraw connections - std::shared_ptr m_featureSet = nullptr; + std::shared_ptr m_featureSet; DeviceId m_deviceID; }; @@ -233,6 +241,10 @@ class SubHidrawConnection : public SubDeviceConnection SubHidrawConnection(Token, const QString& path); + // --- HID++ specific + void initialize(); + void queryBatteryStatus(); + signals: void receivedBatteryInfo(QByteArray batteryData); void receivedPingResponse(); diff --git a/src/devicescan.h b/src/devicescan.h index 04fd407c..5e1619f8 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -4,7 +4,6 @@ #include "device.h" #include -#include #include diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 9a9758da..85021aee 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -31,6 +31,16 @@ namespace { } const auto invalidDeviceId = DeviceId(); // vendorId = 0, productId = 0 + + bool removeTab(QTabWidget* tabWidget, QWidget* widget) + { + const auto idx = tabWidget->indexOf(widget); + if (idx >= 0) { + tabWidget->removeTab(idx); + return true; + } + return false; + } } // ------------------------------------------------------------------------------------------------- @@ -65,65 +75,16 @@ const DeviceId DevicesWidget::currentDeviceId() const } // ------------------------------------------------------------------------------------------------- -QWidget* DevicesWidget::createTimerTabWidget(Settings* settings, Spotlight* spotlight) +TimerTabWidget* DevicesWidget::createTimerTabWidget(Settings* settings, Spotlight* spotlight) { - Q_UNUSED(settings); Q_UNUSED(spotlight); + const auto w = new TimerTabWidget(settings, this); + w->loadSettings(currentDeviceId()); - const auto w = new QWidget(this); - const auto layout = new QVBoxLayout(w); - const auto timerWidget = new MultiTimerWidget(this); - m_vibrationSettingsWidget = new VibrationSettingsWidget(this); - - layout->addWidget(timerWidget); - layout->addWidget(m_vibrationSettingsWidget); - - auto loadSettings = [this, settings, timerWidget](const DeviceId& dId) { - for (int i = 0; i < timerWidget->timerCount(); ++i) { - const auto ts = settings->timerSettings(dId, i); - timerWidget->setTimerEnabled(i, ts.first); - timerWidget->setTimerValue(i, ts.second); - } - const auto vs = settings->vibrationSettings(dId); - m_vibrationSettingsWidget->setLength(vs.first); - m_vibrationSettingsWidget->setIntensity(vs.second); - }; - - loadSettings(currentDeviceId()); - - connect(this, &DevicesWidget::currentDeviceChanged, this, - [loadSettings=std::move(loadSettings), timerWidget, this](const DeviceId& dId) { - timerWidget->stopAllTimers(); - timerWidget->blockSignals(true); - m_vibrationSettingsWidget->blockSignals(true); - loadSettings(dId); - m_vibrationSettingsWidget->blockSignals(false); - timerWidget->blockSignals(false); - }); - - connect(timerWidget, &MultiTimerWidget::timerValueChanged, this, - [timerWidget, settings, this](int id, int secs) { - settings->setTimerSettings(currentDeviceId(), id, timerWidget->timerEnabled(id), secs); - }); - - connect(timerWidget, &MultiTimerWidget::timerEnabledChanged, this, - [timerWidget, settings, this](int id, bool enabled) { - settings->setTimerSettings(currentDeviceId(), id, enabled, timerWidget->timerValue(id)); - }); - - connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::intensityChanged, this, - [settings, this](uint8_t intensity) { - settings->setVibrationSettings(currentDeviceId(), m_vibrationSettingsWidget->length(), intensity); - }); - - connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::lengthChanged, this, - [settings, this](uint8_t len) { - settings->setVibrationSettings(currentDeviceId(), len, m_vibrationSettingsWidget->intensity()); + connect(this, &DevicesWidget::currentDeviceChanged, this, [this](const DeviceId& dId) { + if (m_timerTabWidget) { m_timerTabWidget->loadSettings(dId); } }); - connect(timerWidget, &MultiTimerWidget::timeout, - m_vibrationSettingsWidget, &VibrationSettingsWidget::sendVibrateCommand); - return w; } @@ -141,54 +102,20 @@ QWidget* DevicesWidget::createDevicesWidget(Settings* settings, Spotlight* spotl vLayout->addSpacing(10); - const auto tabWidget = new QTabWidget(dw); - vLayout->addWidget(tabWidget); - - tabWidget->addTab(createInputMapperWidget(settings, spotlight), tr("Input Mapping")); + m_tabWidget = new QTabWidget(dw); + vLayout->addWidget(m_tabWidget); - auto vibrateConn = [spotlight](const DeviceId& devId) { - const auto currentConn = spotlight->deviceConnection(devId); - if (currentConn) { - for (const auto& item : currentConn->subDevices()) { - if ((item.second->flags() & DeviceFlag::Vibrate) == DeviceFlag::Vibrate) return item.second; - } - } - return std::shared_ptr{}; - }; + m_tabWidget->addTab(createInputMapperWidget(settings, spotlight), tr("Input Mapping")); + m_timerTabWidget = createTimerTabWidget(settings, spotlight); - if (const auto conn = vibrateConn(currentDeviceId())) { - m_timerTabWidget = createTimerTabWidget(settings, spotlight); - tabWidget->addTab(m_timerTabWidget, tr("Vibration Timer")); - m_vibrationSettingsWidget->setSubDeviceConnection(conn.get()); - } + updateTimerTab(spotlight); m_deviceDetailsTabWidget = createDeviceInfoWidget(spotlight); - tabWidget->addTab(m_deviceDetailsTabWidget, tr("Details")); + m_tabWidget->addTab(m_deviceDetailsTabWidget, tr("Details")); + // Update the timer tab when the current device has changed connect(this, &DevicesWidget::currentDeviceChanged, this, - [vibrateConn=std::move(vibrateConn), tabWidget, settings, spotlight, this] - (const DeviceId& devId) { - const auto idx = tabWidget->indexOf(m_deviceDetailsTabWidget); - if (idx >= 0) tabWidget->removeTab(idx); - if (const auto conn = vibrateConn(devId)) { - if (m_timerTabWidget == nullptr) { - m_timerTabWidget = createTimerTabWidget(settings, spotlight); - } - if (tabWidget->indexOf(m_timerTabWidget) < 0) { - tabWidget->addTab(m_timerTabWidget, tr("Vibration Timer")); - } - m_vibrationSettingsWidget->setSubDeviceConnection(conn.get()); - } - else if (m_timerTabWidget) { - const auto idx = tabWidget->indexOf(m_timerTabWidget); - if (idx >= 0) tabWidget->removeTab(idx); - m_vibrationSettingsWidget->setSubDeviceConnection(nullptr); - } - // ensure that Details tab is last tab - tabWidget->addTab(m_deviceDetailsTabWidget, tr("Details")); - - tabWidget->setCurrentIndex(0); - }); + [spotlight, this]() { updateTimerTab(spotlight); }); return dw; } @@ -280,11 +207,12 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) return tr("Device not active. Press any key on device to update."); } }; - const bool hasBattery = std::any_of(sDevices.cbegin(), sDevices.cend(), - [](const auto& sd){ - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - !!(sd.second->flags() & DeviceFlag::ReportBattery));}); + const bool hasBattery = std::any_of(sDevices.cbegin(), sDevices.cend(), [](const auto& sd) + { + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + sd.second->hasFlags(DeviceFlag::ReportBattery)); + }); deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); deviceDetails += tr("VendorId:\t%1\n").arg(logging::hexId(dc->deviceId().vendorId)); @@ -484,3 +412,108 @@ QWidget* DevicesWidget::createDisconnectedStateWidget() return stateWidget; } +// ------------------------------------------------------------------------------------------------- +TimerTabWidget::TimerTabWidget(Settings* settings, QWidget* parent) + : QWidget(parent) + , m_settings(settings) + , m_multiTimerWidget(new MultiTimerWidget(this)) + , m_vibrationSettingsWidget(new VibrationSettingsWidget(this)) +{ + const auto layout = new QVBoxLayout(this); + + layout->addWidget(m_multiTimerWidget); + layout->addWidget(m_vibrationSettingsWidget); + + connect(m_multiTimerWidget, &MultiTimerWidget::timerValueChanged, this, + [this](int id, int secs) { + m_settings->setTimerSettings(m_deviceId, id, m_multiTimerWidget->timerEnabled(id), secs); + }); + + connect(m_multiTimerWidget, &MultiTimerWidget::timerEnabledChanged, this, + [this](int id, bool enabled) { + m_settings->setTimerSettings(m_deviceId, id, enabled, m_multiTimerWidget->timerValue(id)); + }); + + connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::intensityChanged, this, + [settings, this](uint8_t intensity) { + m_settings->setVibrationSettings(m_deviceId, m_vibrationSettingsWidget->length(), intensity); + }); + + connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::lengthChanged, this, + [settings, this](uint8_t len) { + m_settings->setVibrationSettings(m_deviceId, len, m_vibrationSettingsWidget->intensity()); + }); + + connect(m_multiTimerWidget, &MultiTimerWidget::timeout, + m_vibrationSettingsWidget, &VibrationSettingsWidget::sendVibrateCommand); +} + +// ------------------------------------------------------------------------------------------------- +void DevicesWidget::updateTimerTab(Spotlight* spotlight) +{ + // Helper method to return the first subconnection that supports vibrate. + auto getVibrateConnection = [](const std::shared_ptr& conn) { + if (conn) { + for (const auto& item : conn->subDevices()) { + if (item.second->hasFlags(DeviceFlag::Vibrate)) return item.second; + } + } + return std::shared_ptr{}; + }; + + const auto currentConn = spotlight->deviceConnection(currentDeviceId()); + const auto vibrateConn = getVibrateConnection(currentConn); + + if (m_timerTabContext) m_timerTabContext->deleteLater(); + + if (vibrateConn) + { + if (m_tabWidget->indexOf(m_timerTabWidget) < 0) { + m_tabWidget->insertTab(1, m_timerTabWidget, tr("Vibration Timer")); + } + m_timerTabWidget->setSubDeviceConnection(vibrateConn.get()); + } + else if (m_timerTabWidget) { + removeTab(m_tabWidget, m_timerTabWidget); + m_timerTabWidget->setSubDeviceConnection(nullptr); + } + m_tabWidget->setCurrentIndex(0); + + if (currentConn) { + m_timerTabContext = QPointer(new QObject(this)); + connect(&*currentConn, &DeviceConnection::subDeviceFlagsChanged, m_timerTabContext, + [currId=currentDeviceId(), spotlight, this](const DeviceId& id, const QString&) { + if (currId != id) return; + updateTimerTab(spotlight); + }); + } + +} + +// ------------------------------------------------------------------------------------------------- +void TimerTabWidget::loadSettings(const DeviceId& deviceId) +{ + m_multiTimerWidget->stopAllTimers(); + m_multiTimerWidget->blockSignals(true); + m_vibrationSettingsWidget->blockSignals(true); + + m_deviceId = deviceId; + + for (int i = 0; i < m_multiTimerWidget->timerCount(); ++i) { + const auto ts = m_settings->timerSettings(deviceId, i); + m_multiTimerWidget->setTimerEnabled(i, ts.first); + m_multiTimerWidget->setTimerValue(i, ts.second); + } + + const auto vs = m_settings->vibrationSettings(deviceId); + m_vibrationSettingsWidget->setLength(vs.first); + m_vibrationSettingsWidget->setIntensity(vs.second); + + m_vibrationSettingsWidget->blockSignals(false); + m_multiTimerWidget->blockSignals(false); +} + +// ------------------------------------------------------------------------------------------------- +void TimerTabWidget::setSubDeviceConnection(SubDeviceConnection* sdc) { + m_vibrationSettingsWidget->setSubDeviceConnection(sdc); +} diff --git a/src/deviceswidget.h b/src/deviceswidget.h index b1dab1ef..55144e36 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -1,16 +1,20 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once +#include "device.h" + #include #include -struct DeviceId; class InputMapper; +class MultiTimerWidget; class QComboBox; class Settings; class Spotlight; class VibrationSettingsWidget; +class TimerTabWidget; +class QTabWidget; class QTimer; class QTextEdit; @@ -33,16 +37,37 @@ class DevicesWidget : public QWidget QWidget* createDevicesWidget(Settings* settings, Spotlight* spotlight); QWidget* createInputMapperWidget(Settings* settings, Spotlight* spotlight); QWidget* createDeviceInfoWidget(Spotlight* spotlight); - QWidget* createTimerTabWidget(Settings* settings, Spotlight* spotlight); + TimerTabWidget* createTimerTabWidget(Settings* settings, Spotlight* spotlight); + void updateTimerTab(Spotlight* spotlight); QComboBox* m_devicesCombo = nullptr; - QWidget* m_timerTabWidget = nullptr; + QTabWidget* m_tabWidget = nullptr; + TimerTabWidget* m_timerTabWidget = nullptr; + QPointer m_timerTabContext; QWidget* m_deviceDetailsTabWidget = nullptr; // TODO Put into separate DeviceDetailsWidget QTextEdit* m_deviceDetailsTextEdit = nullptr; QTimer* m_updateDeviceDetailsTimer = nullptr; - VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; QPointer m_inputMapper; }; + +// ------------------------------------------------------------------------------------------------- +class TimerTabWidget : public QWidget +{ + Q_OBJECT + +public: + TimerTabWidget(Settings* settings, QWidget* parent = nullptr); + VibrationSettingsWidget* vibrationSettingsWidget(); + + void loadSettings(const DeviceId& deviceId); + void setSubDeviceConnection(SubDeviceConnection* sdc); + +private: + DeviceId m_deviceId; + Settings* const m_settings = nullptr; + MultiTimerWidget* m_multiTimerWidget = nullptr; + VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; +}; diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index dcf86a5a..83873c2b 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -9,8 +9,6 @@ #include "settings.h" #include "spotlight.h" -#include -#include #include #include #include diff --git a/src/projecteurapp.h b/src/projecteurapp.h index 8f2dc8dd..1c4ad224 100644 --- a/src/projecteurapp.h +++ b/src/projecteurapp.h @@ -17,7 +17,6 @@ class QQmlApplicationEngine; class QQmlComponent; class QSystemTrayIcon; class Settings; -class Settings; class ProjecteurApplication : public QApplication { diff --git a/src/runguard.h b/src/runguard.h index 3ea97932..03cac1bf 100644 --- a/src/runguard.h +++ b/src/runguard.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include diff --git a/src/spotlight.cc b/src/spotlight.cc index 5879aea3..c1b61e24 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -2,6 +2,7 @@ #include "spotlight.h" #include "deviceinput.h" +#include "hidpp.h" #include "logging.h" #include "settings.h" #include "virtualdevice.h" @@ -132,20 +133,27 @@ int Spotlight::connectDevices() if (dc->hasSubDevice(scanSubDevice.deviceFile)) continue; std::shared_ptr subDeviceConnection = - [&scanSubDevice, &dc, this]() -> std::shared_ptr { + [&scanSubDevice, &dc, this]() -> std::shared_ptr + { // Input event sub devices if (scanSubDevice.type == DeviceScan::SubDevice::Type::Event) { auto devCon = SubEventConnection::create(scanSubDevice, *dc); if (addInputEventHandler(devCon)) return devCon; - } else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { + } // Hidraw sub devices + else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); - if(addHIDInputHandler(hidCon)) { - connect(hidCon.get(), &SubHidrawConnection::receivedBatteryInfo, + if (addHIDInputHandler(hidCon)) + { + // Post initialize task to event loop + hidCon->postTask([c = hidCon.get()](){ c->initialize(); }); + + // connect to hidraw sub connection signals + connect(hidCon.get(), &SubHidrawConnection::receivedBatteryInfo, dc.get(), &DeviceConnection::setBatteryInfo); - connect(hidCon.get(), &SubHidrawConnection::receivedPingResponse, + connect(hidCon.get(), &SubHidrawConnection::receivedPingResponse, dc.get(), [this, dc](){emit deviceActivated(dc->deviceId(), dc->deviceName());}); + return hidCon; } - if(addHIDInputHandler(hidCon)) return hidCon; } return std::shared_ptr(); }(); @@ -248,7 +256,7 @@ void Spotlight::removeDeviceConnection(const QString &devicePath) // ------------------------------------------------------------------------------------------------- void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) { - const bool isNonBlocking = !!(connection.flags() & DeviceFlag::NonBlocking); + const bool isNonBlocking = connection.hasFlags(DeviceFlag::NonBlocking); while (true) { auto& buf = connection.inputBuffer(); @@ -258,7 +266,7 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) if (errno != EAGAIN) { const bool anyConnectedBefore = anySpotlightDeviceConnected(); - connection.disable(); + connection.setNotifiersEnabled(false); QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ removeDeviceConnection(devicePath); if (!anySpotlightDeviceConnected() && anyConnectedBefore) { @@ -310,7 +318,7 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) if (errno != EAGAIN) { const bool anyConnectedBefore = anySpotlightDeviceConnected(); - connection.disable(); + connection.setNotifiersEnabled(false); QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ removeDeviceConnection(devicePath); if (!anySpotlightDeviceConnected() && anyConnectedBefore) { @@ -335,7 +343,7 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) if (connection_status) { // connection between USB dongle and spotlight device broke connection.setHIDProtocol(-1); } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. - if (!connection.isOnline()) connection.initSubDevice(); + if (!connection.isOnline()) connection.initialize(); } } } @@ -353,7 +361,7 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) auto wirelessNotificationID = connection.getFeatureSet()->getFeatureID(FeatureCode::WirelessDeviceStatus); if (wirelessNotificationID && readVal.at(2) == wirelessNotificationID) { // Logitech spotlight presenter unit got online. - if (!connection.isOnline()) connection.initSubDevice(); + if (!connection.isOnline()) connection.initialize(); } // Battery packet processing: Device responded to BatteryStatus (0x1000) packet From 1c814857bea7cbe5fc8ad58f6e9837a953f05c2b Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 31 Jul 2021 17:20:03 +0200 Subject: [PATCH 031/110] Fix Ubuntu 18.04 build. Missing include. --- src/projecteurapp.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index 83873c2b..3477bd83 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -9,6 +9,7 @@ #include "settings.h" #include "spotlight.h" +#include #include #include #include From fb4820f30efd28f973921bbd21e15973d50a6ce3 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 31 Jul 2021 18:28:44 +0200 Subject: [PATCH 032/110] Update asynchronous header for Qt versions lower than 5.10 --- src/asynchronous.h | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/asynchronous.h b/src/asynchronous.h index a3a698c0..406844b6 100644 --- a/src/asynchronous.h +++ b/src/asynchronous.h @@ -4,16 +4,40 @@ #include #include +#if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) +#include +#include +#endif + #include #include namespace async { +#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) // Invoke a (lambda) function for context QObject with queued connection. template void invoke(QObject* context, F function) { QMetaObject::invokeMethod(context, std::move(function), Qt::QueuedConnection); } +#else +// ... older Qt versions < 5.10 +namespace detail { +template +struct FEvent : public QEvent { + using Fun = typename std::decay::type; + Fun fun; + FEvent(Fun && fun) : QEvent(QEvent::None), fun(std::move(fun)) {} + FEvent(const Fun & fun) : QEvent(QEvent::None), fun(fun) {} + ~FEvent() { fun(); } +}; } + +template +void invoke(QObject* context, F&& function) { + QCoreApplication::postEvent(context, new detail::FEvent(std::forward(function))); +} +#endif + // --- Helpers to deduce std::function type from a lambda. template @@ -44,11 +68,16 @@ std::function makeSafeCallback(QObject* context, return; } + #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QMetaObject::invokeMethod(ctxPtr, std::bind(std::move(func), std::forward(args)...), forceQueued ? Qt::QueuedConnection : Qt::AutoConnection); // Note: if forceQueued is false and current thread is the same as // the context thread -> execute directly + #else + // For Qt < 5.10 the call is always queued via the event queue + async::invoke(ctxPtr, std::bind(std::move(func), std::forward(args)...)); + #endif }; return res; } @@ -83,7 +112,7 @@ class Async /// Post a function to the own event loop. template void postSelf(F function) { - invoke(static_cast(this), std::move(function)); + async::invoke(static_cast(this), std::move(function)); } public: From 70a68e4159fc5199b3bcddc14a91424e252a655d Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 1 Aug 2021 18:25:30 +0200 Subject: [PATCH 033/110] Restructure HID++, work in progress - encapsulating Hid++ functionality. --- src/device-vibration.cc | 17 +- src/device.cc | 386 +++++++++++++++++++++++++--------------- src/device.h | 84 ++++++--- src/hidpp.cc | 43 +++-- src/hidpp.h | 71 ++++---- src/spotlight.cc | 48 ++--- src/spotlight.h | 4 +- 7 files changed, 407 insertions(+), 246 deletions(-) diff --git a/src/device-vibration.cc b/src/device-vibration.cc index 576613c5..97c3aaf3 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -412,23 +412,10 @@ void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc) void VibrationSettingsWidget::sendVibrateCommand() { if (!m_subDeviceConnection) return; - if (!m_subDeviceConnection->hasFlags(DeviceFlag::Vibrate)) return; if (!m_subDeviceConnection->isConnected()) return; - - // TODO generalize features and protocol for proprietary device features like vibration - // for not only the Spotlight device. - // - // Spotlight: - // present - // controlID len intensity - // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; - - const uint8_t pcID = m_subDeviceConnection->getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); - if (pcID == 0x00) return; + if (!m_subDeviceConnection->hasFlags(DeviceFlag::Vibrate)) return; const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); - const uint8_t vibrateCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, pcID, 0x1d, vlen, 0xe8, vint}; - - m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); + m_subDeviceConnection->sendVibrateCommand(vint, vlen); } diff --git a/src/device.cc b/src/device.cc index 4cea9a26..79cdb141 100644 --- a/src/device.cc +++ b/src/device.cc @@ -21,11 +21,60 @@ namespace { static const auto registeredComparator_ = QMetaType::registerComparators(); const auto hexId = logging::hexId; + class i18n : public QObject {}; // for i18n and logging // ----------------------------------------------------------------------------------------------- - bool deviceHasHidppSupport(const DeviceId& id) { - // HID++ only for Logitech devices - return id.vendorId == 0x046d; + /// Open a hidraw subdevice and + int openHidrawSubDevice(const DeviceScan::SubDevice& sd, const DeviceId& devId) + { + constexpr int errorResult = -1; + const int devfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDWR|O_NONBLOCK , 0); + + if (devfd == errorResult) { + logWarn(device) << i18n::tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile); + return errorResult; + } + + { // Get Report Descriptor Size and Descriptor -- currently unused, but if it fails + // we don't use the device + int descriptorSize = 0; + if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0) + { + logWarn(device) << i18n::tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); + ::close(devfd); + return errorResult; + } + + struct hidraw_report_descriptor reportDescriptor {}; + reportDescriptor.size = descriptorSize; + if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0) + { + logWarn(device) << i18n::tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile); + ::close(devfd); + return errorResult; + } + } + + struct hidraw_devinfo devinfo {}; + // get the hidraw sub-device id info + if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0) + { + logWarn(device) << i18n::tr("Cannot get info from hidraw device '%1'.").arg(sd.deviceFile); + ::close(devfd); + return errorResult; + }; + + // Check against given device id + if (static_cast(devinfo.vendor) != devId.vendorId + || static_cast(devinfo.product) != devId.productId) + { + logDebug(device) << i18n::tr("Device id mismatch: %1 (%2:%3)") + .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product)); + ::close(devfd); + return errorResult; + } + + return devfd; } } @@ -35,7 +84,6 @@ DeviceConnection::DeviceConnection(const DeviceId& id, const QString& name, : m_deviceId(id) , m_deviceName(name) , m_inputMapper(std::make_shared(std::move(vdev))) - , m_featureSet(std::make_shared()) { } @@ -80,6 +128,13 @@ bool DeviceConnection::removeSubDevice(const QString& path) return false; } + +// ------------------------------------------------------------------------------------------------- +bool DeviceConnection::hasHidppSupport() const{ + // HID++ only for Logitech devices + return m_deviceId.vendorId == 0x046d; +} + // ------------------------------------------------------------------------------------------------- void DeviceConnection::queryBatteryStatus() { @@ -96,7 +151,7 @@ void DeviceConnection::queryBatteryStatus() // ------------------------------------------------------------------------------------------------- void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) { - if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus) && batteryData.length() == 3) + if (false) //if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus) && batteryData.length() == 3) { // Battery percent is only meaningful when battery is discharging. However, save them anyway. m_batteryInfo.currentLevel = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0): 100); @@ -106,8 +161,16 @@ void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) } // ------------------------------------------------------------------------------------------------- -SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode) - : m_details(path, type, mode) {} +SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, + const DeviceId& id, ConnectionType type, + ConnectionMode mode) + : type(type), mode(mode), busType(id.busType), phys(sd.phys), devicePath(sd.deviceFile) +{} + +// ------------------------------------------------------------------------------------------------- +SubDeviceConnection::SubDeviceConnection(const DeviceScan::SubDevice& sd, const DeviceId& id, + ConnectionType type, ConnectionMode mode) + : m_details(sd, id, type, mode) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; @@ -175,8 +238,23 @@ QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { } // ------------------------------------------------------------------------------------------------- -SubEventConnection::SubEventConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly) {} +ssize_t SubDeviceConnection::sendData(const QByteArray&) { + // do nothing for the base implementation + return -1; +} + +// ------------------------------------------------------------------------------------------------- +ssize_t SubDeviceConnection::sendData(const void*, size_t) { + // do nothing for the base implementation + return -1; +} + +// ------------------------------------------------------------------------------------------------- +void SubDeviceConnection::sendVibrateCommand(uint8_t, uint8_t) {} + +// ------------------------------------------------------------------------------------------------- +SubEventConnection::SubEventConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id) + : SubDeviceConnection(sd, id, ConnectionType::Event, ConnectionMode::ReadOnly) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd, @@ -210,8 +288,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: return std::shared_ptr(); } - auto connection = std::make_shared(Token{}, sd.deviceFile); - connection->m_deviceID = dc.deviceId(); + auto connection = std::make_shared(Token{}, sd, dc.deviceId()); if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; @@ -259,114 +336,34 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: }); connection->m_inputMapper = dc.inputMapper(); - connection->m_details.phys = sd.phys; - return connection; } // ------------------------------------------------------------------------------------------------- -SubHidrawConnection::SubHidrawConnection(Token, const QString& path) - : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} +SubHidrawConnection::SubHidrawConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id) + : SubDeviceConnection(sd, id, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc) { - const int devfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDWR|O_NONBLOCK , 0); - - if (devfd == -1) { - logWarn(device) << tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile); - return std::shared_ptr(); - } + const int devfd = openHidrawSubDevice(sd, dc.deviceId()); + if (devfd == -1) return std::shared_ptr(); - int descriptorSize = 0; - // Get Report Descriptor Size - if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0) { - logWarn(device) << tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); - return std::shared_ptr(); - } - - struct hidraw_report_descriptor reportDescriptor{}; - reportDescriptor.size = descriptorSize; - if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0) { - logWarn(device) << tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile); - return std::shared_ptr(); - } - - struct hidraw_devinfo devinfo{}; - // get the hidraw sub-device id info - if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0) { - logWarn(device) << tr("Cannot get info from hidraw device '%1'.").arg(sd.deviceFile); - return std::shared_ptr(); - }; - - // Check against given device id - if (static_cast(devinfo.vendor) != dc.deviceId().vendorId - || static_cast(devinfo.product) != dc.deviceId().productId) - { - ::close(devfd); - logDebug(device) << tr("Device id mismatch: %1 (%2:%3)") - .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product)); - return std::shared_ptr(); - } - - auto connection = std::make_shared(Token{}, sd.deviceFile); - connection->m_deviceID = dc.deviceId(); - - // TODO feature set needs to be a member of a sub hidraw connection - connection->m_featureSet = dc.getFeatureSet(); - connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); - // --- - - fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK); - if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { - connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; - } - - if (deviceHasHidppSupport(dc.deviceId())) { - connection->m_details.deviceFlags |= DeviceFlag::Hidpp; - } - - // Create read and write socket notifiers - connection->m_readNotifier = std::make_unique(devfd, QSocketNotifier::Read); - QSocketNotifier* const readNotifier = connection->m_readNotifier.get(); - // Auto clean up and close descriptor on destruction of notifier - connect(readNotifier, &QSocketNotifier::destroyed, [readNotifier]() { - ::close(static_cast(readNotifier->socket())); - }); - - connection->m_writeNotifier = std::make_unique(devfd, QSocketNotifier::Write); - QSocketNotifier* const writeNotifier = connection->m_writeNotifier.get(); - writeNotifier->setEnabled(false); // Disable write notifier by default - // Auto clean up and close descriptor on destruction of notifier - connect(writeNotifier, &QSocketNotifier::destroyed, [writeNotifier]() { - ::close(static_cast(writeNotifier->socket())); - }); - - connection->m_details.phys = sd.phys; + auto connection = std::make_shared(Token{}, sd, dc.deviceId()); + connection->createSocketNotifiers(devfd); return connection; } // ------------------------------------------------------------------------------------------------- -void SubHidrawConnection::queryBatteryStatus() -{ - if (hasFlags(DeviceFlag::ReportBattery)) - { - const uint8_t batteryFeatureID = m_featureSet->getFeatureID(FeatureCode::BatteryStatus); - if (batteryFeatureID) - { - const uint8_t batteryCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; - sendData(batteryCmd, sizeof(batteryCmd), false); - } - } -} +void SubHidrawConnection::queryBatteryStatus() {} // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::pingSubDevice() { constexpr uint8_t rootID = 0x00; // root ID is always 0x00 in any logitech device - const uint8_t pingCmd[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, rootID, 0x1d, 0x00, 0x00, 0x5d}; - sendData(pingCmd, sizeof(pingCmd), false); + const uint8_t pingCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rootID, 0x1d, 0x00, 0x00, 0x5d}; + sendData(pingCmd, sizeof(pingCmd)); } // ------------------------------------------------------------------------------------------------- @@ -380,7 +377,7 @@ void SubDeviceConnection::setHIDProtocol(float version) { } // ------------------------------------------------------------------------------------------------- -void SubHidrawConnection::initialize() +void SubHidppConnection::initialize() { // Currently only HID++ devices need additional initializing if (!hasFlags(DeviceFlag::Hidpp)) return; @@ -388,29 +385,29 @@ void SubHidrawConnection::initialize() constexpr int delay_ms = 20; int msgCount = 0; // Reset device: get rid of any device configuration by other programs ------- - if (m_deviceID.busType == BusType::Usb) + if (m_details.busType == BusType::Usb) { - // Reset USB dongle QTimer::singleShot(delay_ms*msgCount, this, [this](){ - constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_GET_FEATURE, 0x00, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); + const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_GET_FEATURE, 0x00, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data)); + }); msgCount++; // Turn off software bit and keep the wireless notification bit on QTimer::singleShot(delay_ms*msgCount, this, [this](){ - constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x01, 0x00}; - sendData(data, sizeof(data), false);}); + constexpr uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_SET_FEATURE, 0x00, 0x00, 0x01, 0x00}; + sendData(data, sizeof(data));}); msgCount++; // Initialize USB dongle QTimer::singleShot(delay_ms*msgCount, this, [this](){ - constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x02, 0x02, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); + constexpr uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_SET_FEATURE, 0x02, 0x02, 0x00, 0x00}; + sendData(data, sizeof(data));}); msgCount++; QTimer::singleShot(delay_ms*msgCount, this, [this](){ - constexpr uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_USB_RECEIVER, HIDPP_SHORT_SET_FEATURE, 0x00, 0x00, 0x09, 0x00}; - sendData(data, sizeof(data), false);}); + constexpr uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_SET_FEATURE, 0x00, 0x00, 0x09, 0x00}; + sendData(data, sizeof(data));}); msgCount++; } @@ -439,85 +436,182 @@ void SubHidrawConnection::initialize() const auto resetID = m_featureSet->getFeatureID(FeatureCode::Reset); if (resetID) { QTimer::singleShot(delay_ms*msgCount, this, [this, resetID](){ - const uint8_t data[] = {HIDPP_SHORT_MSG, MSG_TO_SPOTLIGHT, resetID, 0x1d, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data), false);}); + const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, resetID, 0x1d, 0x00, 0x00, 0x00}; + sendData(data, sizeof(data));}); msgCount++; } } // Device Resetting complete ------------------------------------------------- - if (m_deviceID.busType == BusType::Usb) { + if (m_details.busType == BusType::Usb) { // Ping spotlight device for checking if is online // the response will have the version for HID++ protocol. QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); msgCount++; - } else if (m_deviceID.busType == BusType::Bluetooth) { + } else if (m_details.busType == BusType::Bluetooth) { // Bluetooth connection mean HID++ v2.0+. // Setting version to 6.4: same as USB connection. setHIDProtocol(6.4); } setFlags(featureFlags, true); + // Add other configuration to enable features in device // like enabling on Next and back button on hold functionality. } // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg, bool checkDeviceOnline) +ssize_t SubHidrawConnection::sendData(const QByteArray& msg) +{ + constexpr ssize_t errorResult = -1; + + if (mode() != ConnectionMode::ReadWrite || !m_writeNotifier) { return errorResult; } + + const auto notifier = socketWriteNotifier(); + const auto res = ::write(notifier->socket(), msg.data(), msg.length()); + + if (res == msg.length()) { + logDebug(hid) << res << "bytes written to" << path() << "(" << msg.toHex() << ")"; + } else { + logWarn(hid) << "Writing to" << path() << "failed."; + } + + return res; +} + +// ------------------------------------------------------------------------------------------------- +ssize_t SubHidrawConnection::sendData(const void* msg, size_t msgLen) { - ssize_t res = -1; + const QByteArray msgArr(reinterpret_cast(msg), msgLen); + return sendData(msgArr); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidrawConnection::createSocketNotifiers(int fd) +{ + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + if ((fcntl(fd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { + m_details.deviceFlags |= DeviceFlag::NonBlocking; + } + + // Create read and write socket notifiers + m_readNotifier = std::make_unique(fd, QSocketNotifier::Read); + QSocketNotifier *const readNotifier = m_readNotifier.get(); + // Auto clean up and close descriptor on destruction of notifier + connect(readNotifier, &QSocketNotifier::destroyed, [readNotifier]() { + ::close(static_cast(readNotifier->socket())); + }); + + m_writeNotifier = std::make_unique(fd, QSocketNotifier::Write); + QSocketNotifier *const writeNotifier = m_writeNotifier.get(); + writeNotifier->setEnabled(false); // Disable write notifier by default + // Auto clean up and close descriptor on destruction of notifier + connect(writeNotifier, &QSocketNotifier::destroyed, [writeNotifier]() { + ::close(static_cast(writeNotifier->socket())); + }); +} + +// ------------------------------------------------------------------------------------------------- +SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token, + const DeviceScan::SubDevice &sd, const DeviceId &id) + : SubHidrawConnection(token, sd, id), m_featureSet(std::make_unique()) {} + +// ------------------------------------------------------------------------------------------------- +SubHidppConnection::~SubHidppConnection() = default; + +// ------------------------------------------------------------------------------------------------- + ssize_t SubHidppConnection::sendData(const QByteArray &msg) +{ + constexpr ssize_t errorResult = -1; + + if (!HIDPP::isValidMessage(msg)) { return errorResult; } // If the message have 0xff as second byte, it is meant for USB dongle hence, // should not be send when device is connected on bluetooth. // - // // Logitech Spotlight (USB) can receive data in two different length. - // 1. Short (10 byte long starting with 0x10) + // 1. Short (7 byte long starting with 0x10) // 2. Long (20 byte long starting with 0x11) // However, bluetooth connection only accepts data in long (20 byte) packets. // For converting standard short length data to long length data, change the first byte to 0x11 and // pad the end of message with 0x00 to acheive the length of 20. - QByteArray _hidppMsg(hidppMsg); - if (m_deviceID.busType == BusType::Bluetooth) { - if (static_cast(hidppMsg.at(1)) == MSG_TO_USB_RECEIVER){ - logDebug(hid) << "Invalid packet" << hidppMsg.toHex() << "for spotlight connected on bluetooth."; - return res; + if (m_details.busType == BusType::Bluetooth) + { + if (HIDPP::isMessageForUsb(msg)) + { + logWarn(hid) << "Invalid packet" << msg.toHex() << "for spotlight connected on bluetooth."; + return errorResult; } - if (hidppMsg.at(0) == HIDPP_SHORT_MSG) { - _hidppMsg = HIDPP::shortToLongMsg(hidppMsg); + // For bluetooth always convert to a long message if we have a short message + if (HIDPP::isValidShortMessage(msg)) { + return SubHidrawConnection::sendData(HIDPP::shortToLongMsg(msg)); } } - bool isValidMsg = (_hidppMsg.length() == 7 && _hidppMsg.at(0) == HIDPP_SHORT_MSG); // HID++ short message - isValidMsg = isValidMsg || (_hidppMsg.length() == 20 && _hidppMsg.at(0) == HIDPP_LONG_MSG); // HID++ long message + return SubHidrawConnection::sendData(msg); +} - // If checkDeviceOnline is true then do not send the packet if device is not online/active. - if (checkDeviceOnline && !isOnline()) { - logInfo(hid) << "The device is not active. Activate it by pressing any button on device."; - return res; +// ------------------------------------------------------------------------------------------------- +std::shared_ptr SubHidppConnection::create(const DeviceScan::SubDevice &sd, + const DeviceConnection &dc) +{ + const int devfd = openHidrawSubDevice(sd, dc.deviceId()); + if (devfd == -1) return std::shared_ptr(); + + auto connection = std::make_shared(Token{}, sd, dc.deviceId()); + + // TODO feature set needs to be a member of a sub hidraw connection + // connection->m_featureSet = dc.getFeatureSet(); + connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); + // --- + + if (dc.hasHidppSupport()) { + connection->m_details.deviceFlags |= DeviceFlag::Hidpp; } - if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite - && m_writeNotifier && isValidMsg) - { - const auto notifier = socketWriteNotifier(); - res = ::write(notifier->socket(), _hidppMsg.data(), _hidppMsg.length()); + connection->createSocketNotifiers(devfd); + connection->postTask([c=&*connection](){ c->initialize(); }); + return connection; +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) +{ + // TODO put in HIDPP - if (res == _hidppMsg.length()) { - logDebug(hid) << "Write" << _hidppMsg.toHex() << "to" << path(); - } else { - logWarn(hid) << "Writing to" << path() << "failed."; + // TODO generalize features and protocol for proprietary device features like vibration + // for not only the Spotlight device. + // + // Spotlight: + // present + // controlID len intensity + // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; + + const uint8_t pcID = getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); + if (pcID == 0x00) return; + const uint8_t vibrateCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, pcID, 0x1d, length, 0xe8, intensity}; + sendData(vibrateCmd, sizeof(vibrateCmd)); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::queryBatteryStatus() +{ + // TODO put parts in HIDPP + if (hasFlags(DeviceFlag::ReportBattery)) + { + const uint8_t batteryFeatureID = m_featureSet->getFeatureID(FeatureCode::BatteryStatus); + if (batteryFeatureID) + { + const uint8_t batteryCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; + sendData(batteryCmd, sizeof(batteryCmd)); } } - - return res; } // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline) +const HIDPP::FeatureSet* SubHidppConnection::getFeatureSet() { - const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen); - return sendData(hidppMsgArr, checkDeviceOnline); + return &*m_featureSet; } diff --git a/src/device.h b/src/device.h index a20d2c6b..589c0a98 100644 --- a/src/device.h +++ b/src/device.h @@ -13,7 +13,7 @@ // ------------------------------------------------------------------------------------------------- // Bus on which device is connected -enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; +enum class BusType : uint8_t { Unknown, Usb, Bluetooth }; // ------------------------------------------------------------------------------------------------- struct DeviceId @@ -44,7 +44,14 @@ class InputMapper; class QSocketNotifier; class SubDeviceConnection; class VirtualDevice; -class FeatureSet; + +namespace HIDPP { + class FeatureSet; +} + +namespace DeviceScan { + struct SubDevice; +} // ------------------------------------------------------------------------------------------------- // Battery Status as returned on HID++ BatteryStatus feature code (0x1000) @@ -81,7 +88,7 @@ class DeviceConnection : public QObject const auto& deviceName() const { return m_deviceName; } const auto& deviceId() const { return m_deviceId; } const auto& inputMapper() const { return m_inputMapper; } - const auto& getFeatureSet() const { return m_featureSet; } + bool hasHidppSupport() const; auto subDeviceCount() const { return m_subDeviceConnections.size(); } bool hasSubDevice(const QString& path) const; @@ -108,7 +115,6 @@ public slots: std::shared_ptr m_inputMapper; ConnectionMap m_subDeviceConnections; BatteryInfo m_batteryInfo; - std::shared_ptr m_featureSet; }; // ------------------------------------------------------------------------------------------------- @@ -126,13 +132,14 @@ enum class DeviceFlag : uint32_t { }; ENUM(DeviceFlag, DeviceFlags) -// ----------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { - SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode) - : type(type), mode(mode), devicePath(path) {} + SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, const DeviceId& id, + ConnectionType type, ConnectionMode mode); ConnectionType type; ConnectionMode mode; + BusType busType = BusType::Unknown; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString phys; @@ -170,14 +177,14 @@ class SubDeviceConnection : public QObject, public async::Async 0); }; void setHIDProtocol(float version); float getHIDProtocol() const { return m_details.hidProtocolVer; }; - ssize_t sendData(const QByteArray& hidppMsg, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection - ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen, bool checkDeviceOnline = true); // Send HID++ Message to HIDraw connection + // Base implementation of generic write methods to the device does nothing. + virtual ssize_t sendData(const QByteArray& msg); + virtual ssize_t sendData(const void* msg, size_t msgLen); auto type() const { return m_details.type; }; auto mode() const { return m_details.mode; }; @@ -192,26 +199,22 @@ class SubDeviceConnection : public QObject, public async::Async m_inputMapper; // shared input mapper from parent device. std::unique_ptr m_readNotifier; std::unique_ptr m_writeNotifier; // only useful for Hidraw connections - std::shared_ptr m_featureSet; - DeviceId m_deviceID; }; -// ------------------------------------------------------------------------------------------------- -namespace DeviceScan { - struct SubDevice; -} - // ------------------------------------------------------------------------------------------------- class SubEventConnection : public SubDeviceConnection { @@ -222,7 +225,7 @@ class SubEventConnection : public SubDeviceConnection static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); - SubEventConnection(Token, const QString& path); + SubEventConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id); auto& inputBuffer() { return m_inputEventBuffer; } protected: @@ -233,19 +236,54 @@ class SubEventConnection : public SubDeviceConnection class SubHidrawConnection : public SubDeviceConnection { Q_OBJECT + +protected: class Token{}; public: static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); - SubHidrawConnection(Token, const QString& path); + SubHidrawConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id); - // --- HID++ specific - void initialize(); - void queryBatteryStatus(); + // Generic plain sendData implementation for hidraw devices. + ssize_t sendData(const QByteArray& msg) override; + ssize_t sendData(const void* msg, size_t msgLen) override; + + virtual void queryBatteryStatus(); signals: void receivedBatteryInfo(QByteArray batteryData); void receivedPingResponse(); + +protected: + void createSocketNotifiers(int fd); +}; + +// ------------------------------------------------------------------------------------------------- +class SubHidppConnection : public SubHidrawConnection +{ + Q_OBJECT + +public: + static std::shared_ptr create(const DeviceScan::SubDevice &sd, + const DeviceConnection &dc); + + SubHidppConnection(SubHidrawConnection::Token, const DeviceScan::SubDevice& sd, const DeviceId& id); + ~SubHidppConnection(); + + using SubHidrawConnection::sendData; + + /// sendData implementation for HIDPP devices + ssize_t sendData(const QByteArray& msg) override; + + void queryBatteryStatus() override; + void sendVibrateCommand(uint8_t intensity, uint8_t length) override; + + void initialize(); + + const HIDPP::FeatureSet* getFeatureSet(); + +private: + std::unique_ptr m_featureSet; }; diff --git a/src/hidpp.cc b/src/hidpp.cc index 3d43fe15..e195f86d 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -20,6 +20,7 @@ namespace { class Hid_ : public QObject {}; // for i18n and logging } +namespace HIDPP { // ------------------------------------------------------------------------------------------------- QByteArray FeatureSet::getResponseFromDevice(const QByteArray& expectedBytes) { @@ -47,7 +48,7 @@ uint8_t FeatureSet::getFeatureIDFromDevice(FeatureCode fc) const uint8_t fSetMSB = static_cast(static_cast(fc)); const auto featureReqMessage = make_QByteArray(HidppMsg{ - HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); @@ -72,7 +73,7 @@ uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t featureSetID) // Get Number of features (except Root Feature) supported const auto featureCountReqMessage = make_QByteArray(HidppMsg{ - HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); @@ -101,7 +102,7 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() // Get the number of firmwares (Main HID++ application, BootLoader, or Hardware) now const auto fwCountReqMessage = make_QByteArray(HidppMsg{ - HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); @@ -135,7 +136,7 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() for (uint8_t i = 0x00; i < fwCount; i++) { const auto fwVerReqMessage = make_QByteArray(HidppMsg{ - HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, fwID, 0x1d, i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwID, 0x1d, i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); @@ -199,7 +200,7 @@ void FeatureSet::populateFeatureTable() for (uint8_t featureId = 0x01; featureId <= featureCount; ++featureId) { const auto featureCodeReqMsg = make_QByteArray(HidppMsg{ - HIDPP_LONG_MSG, MSG_TO_SPOTLIGHT, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); const auto res = ::write(m_fdHIDDevice, featureCodeReqMsg.data(), featureCodeReqMsg.size()); @@ -223,14 +224,14 @@ void FeatureSet::populateFeatureTable() } // ------------------------------------------------------------------------------------------------- -bool FeatureSet::supportFeatureCode(FeatureCode fc) +bool FeatureSet::supportFeatureCode(FeatureCode fc) const { const auto featurePair = m_featureTable.find(static_cast(fc)); return (featurePair != m_featureTable.end()); } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureID(FeatureCode fc) +uint8_t FeatureSet::getFeatureID(FeatureCode fc) const { if (!supportFeatureCode(fc)) return 0x00; @@ -239,15 +240,15 @@ uint8_t FeatureSet::getFeatureID(FeatureCode fc) } // ------------------------------------------------------------------------------------------------- -QByteArray HIDPP::shortToLongMsg(const QByteArray& shortMsg) +QByteArray shortToLongMsg(const QByteArray& shortMsg) { - const bool isValidShortMsg = (shortMsg.at(0) == HIDPP_SHORT_MSG && shortMsg.length() == 7); + const bool isValidShortMsg = (shortMsg.at(0) == Bytes::SHORT_MSG && shortMsg.length() == 7); if (isValidShortMsg) { QByteArray longMsg; longMsg.reserve(20); - longMsg.append(HIDPP_LONG_MSG); + longMsg.append(Bytes::LONG_MSG); longMsg.append(shortMsg.mid(1)); longMsg.append(20 - longMsg.length(), 0); return longMsg; @@ -255,3 +256,25 @@ QByteArray HIDPP::shortToLongMsg(const QByteArray& shortMsg) return shortMsg; } + +// ------------------------------------------------------------------------------------------------- +bool isValidMessage(const QByteArray& msg) { + return (isValidShortMessage(msg) || isValidLongMessage(msg)); +} + +// ------------------------------------------------------------------------------------------------- +bool isValidShortMessage(const QByteArray& msg) { + return (msg.length() == 7 && static_cast(msg.at(0)) == Bytes::SHORT_MSG); +} + +// ------------------------------------------------------------------------------------------------- +bool isValidLongMessage(const QByteArray& msg) { + return (msg.length() == 20 && static_cast(msg.at(0)) == Bytes::LONG_MSG); +} + +// ------------------------------------------------------------------------------------------------- +bool isMessageForUsb(const QByteArray& msg) { + return (static_cast(msg.at(1)) == Bytes::MSG_TO_USB_RECEIVER); +} + +} // end namespace HIDPP \ No newline at end of file diff --git a/src/hidpp.h b/src/hidpp.h index f9469575..afa4855f 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -5,17 +5,6 @@ #include -#define HIDPP_SHORT_MSG 0x10 -#define HIDPP_LONG_MSG 0x11 - -#define MSG_TO_USB_RECEIVER 0xff -#define MSG_TO_SPOTLIGHT 0x01 // Spotlight is first device on the receiver (bluetooth also uses this code) - -#define HIDPP_SHORT_GET_FEATURE 0x81 -#define HIDPP_SHORT_SET_FEATURE 0x80 - -#define HIDPP_SHORT_WIRELESS_NOTIFICATION_CODE 0x41 - // Feature Codes important for Logitech Spotlight @@ -36,26 +25,48 @@ enum class FeatureCode : uint16_t { }; namespace HIDPP { + // ----------------------------------------------------------------------------------------------- + namespace Bytes { + constexpr uint8_t SHORT_MSG = 0x10; + constexpr uint8_t LONG_MSG = 0x11; + + constexpr uint8_t MSG_TO_USB_RECEIVER = 0xff; + constexpr uint8_t MSG_TO_SPOTLIGHT = 0x01; // Spotlight is first device on the receiver (bluetooth also uses this code) + + constexpr uint8_t SHORT_GET_FEATURE = 0x81; + constexpr uint8_t SHORT_SET_FEATURE = 0x80; + + constexpr uint8_t SHORT_WIRELESS_NOTIFICATION_CODE = 0x41; + } + /// Used for Bluetooth connections QByteArray shortToLongMsg(const QByteArray& shortMsg); + + /// Returns if msg is a valid hidpp message + bool isValidMessage(const QByteArray& msg); + bool isValidShortMessage(const QByteArray& msg); + bool isValidLongMessage(const QByteArray& msg); + + bool isMessageForUsb(const QByteArray& msg); + + // Class to get and store Set of supported features for a HID++ 2.0 device + class FeatureSet + { + public: + void setHIDDeviceFileDescriptor(int fd) { m_fdHIDDevice = fd; } + uint8_t getFeatureID(FeatureCode fc) const; + bool supportFeatureCode(FeatureCode fc) const; + auto getFeatureCount() const { return m_featureTable.size(); } + void populateFeatureTable(); + + private: + uint8_t getFeatureIDFromDevice(FeatureCode fc); + uint8_t getFeatureCountFromDevice(uint8_t featureSetID); + QByteArray getFirmwareVersionFromDevice(); + QByteArray getResponseFromDevice(const QByteArray &expectedBytes); + + std::map m_featureTable; + int m_fdHIDDevice = -1; + }; } -// Class to get and store Set of supported features for a HID++ 2.0 device -class FeatureSet -{ -public: - void setHIDDeviceFileDescriptor(int fd) { m_fdHIDDevice = fd; } - uint8_t getFeatureID(FeatureCode fc); - bool supportFeatureCode(FeatureCode fc); - auto getFeatureCount() { return m_featureTable.size(); } - void populateFeatureTable(); - -private: - uint8_t getFeatureIDFromDevice(FeatureCode fc); - uint8_t getFeatureCountFromDevice(uint8_t featureSetID); - QByteArray getFirmwareVersionFromDevice(); - QByteArray getResponseFromDevice(const QByteArray& expectedBytes); - - std::map m_featureTable; - int m_fdHIDDevice = -1; -}; diff --git a/src/spotlight.cc b/src/spotlight.cc index c1b61e24..bc76001c 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -139,20 +139,24 @@ int Spotlight::connectDevices() auto devCon = SubEventConnection::create(scanSubDevice, *dc); if (addInputEventHandler(devCon)) return devCon; } // Hidraw sub devices - else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { - auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); - if (addHIDInputHandler(hidCon)) + else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) + { + if (dc->hasHidppSupport()) { - // Post initialize task to event loop - hidCon->postTask([c = hidCon.get()](){ c->initialize(); }); - - // connect to hidraw sub connection signals - connect(hidCon.get(), &SubHidrawConnection::receivedBatteryInfo, - dc.get(), &DeviceConnection::setBatteryInfo); - connect(hidCon.get(), &SubHidrawConnection::receivedPingResponse, - dc.get(), [this, dc](){emit deviceActivated(dc->deviceId(), dc->deviceName());}); + auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc); + if (addHidppInputHandler(hidppCon)) + { + // connect to hidraw sub connection signals + connect(&*hidppCon, &SubHidrawConnection::receivedBatteryInfo, + dc.get(), &DeviceConnection::setBatteryInfo); + connect(&*hidppCon, &SubHidrawConnection::receivedPingResponse, dc.get(), + [this, dc]() { emit deviceActivated(dc->deviceId(), dc->deviceName()); }); - return hidCon; + return hidppCon; + } + } + else { + return SubHidrawConnection::create(scanSubDevice, *dc); } } return std::shared_ptr(); @@ -310,8 +314,10 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) } // ------------------------------------------------------------------------------------------------- -void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) +void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) { + Q_UNUSED(fd); + Q_UNUSED(connection); QByteArray readVal(20, 0); if (::read(fd, static_cast(readVal.data()), readVal.length()) < 0) { @@ -330,15 +336,15 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) - if (!(readVal.at(0) == HIDPP_SHORT_MSG || readVal.at(0) == HIDPP_LONG_MSG)) { + if (!(readVal.at(0) == HIDPP::Bytes::SHORT_MSG || readVal.at(0) == HIDPP::Bytes::LONG_MSG)) { return; } logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - if (readVal.at(0) == HIDPP_SHORT_MSG) // Logitech HIDPP SHORT message: 7 byte long + if (readVal.at(0) == HIDPP::Bytes::SHORT_MSG) // Logitech HIDPP SHORT message: 7 byte long { - if (readVal.at(2) == HIDPP_SHORT_WIRELESS_NOTIFICATION_CODE) { // wireless notification from USB dongle + if (readVal.at(2) == HIDPP::Bytes::SHORT_WIRELESS_NOTIFICATION_CODE) { // wireless notification from USB dongle auto connection_status = readVal.at(4) & (1<<6); // should be zero for successful connection if (connection_status) { // connection between USB dongle and spotlight device broke connection.setHIDProtocol(-1); @@ -348,7 +354,7 @@ void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) } } - if (readVal.at(0) == HIDPP_LONG_MSG) // Logitech HIDPP LONG message: 20 byte long + if (readVal.at(0) == HIDPP::Bytes::LONG_MSG) // Logitech HIDPP LONG message: 20 byte long { auto rootID = connection.getFeatureSet()->getFeatureID(FeatureCode::Root); if (readVal.at(2) == rootID) { @@ -398,16 +404,18 @@ bool Spotlight::addInputEventHandler(std::shared_ptr connect } // ------------------------------------------------------------------------------------------------- -bool Spotlight::addHIDInputHandler(std::shared_ptr connection) +bool Spotlight::addHidppInputHandler(std::shared_ptr connection) { - if (!connection || connection->type() != ConnectionType::Hidraw || !connection->isConnected()) { + if (!connection || connection->type() != ConnectionType::Hidraw + || !connection->isConnected() || !connection->hasFlags(DeviceFlag::Hidpp)) + { return false; } QSocketNotifier* const readNotifier = connection->socketReadNotifier(); connect(readNotifier, &QSocketNotifier::activated, this, [this, connection=std::move(connection)](int fd) { - onHIDDataAvailable(fd, *connection.get()); + onHidppDataAvailable(fd, *connection.get()); }); return true; diff --git a/src/spotlight.h b/src/spotlight.h index a075bbf4..1c25e6c5 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -55,13 +55,13 @@ class Spotlight : public QObject ConnectionResult connectSpotlightDevice(const QString& devicePath, bool verbose = false); bool addInputEventHandler(std::shared_ptr connection); - bool addHIDInputHandler(std::shared_ptr connection); + bool addHidppInputHandler(std::shared_ptr connection); bool setupDevEventInotify(); int connectDevices(); void removeDeviceConnection(const QString& devicePath); void onEventDataAvailable(int fd, SubEventConnection& connection); - void onHIDDataAvailable(int fd, SubHidrawConnection& connection); + void onHidppDataAvailable(int fd, SubHidppConnection& connection); const Options m_options; std::map> m_deviceConnections; From bba71e0c361d313704cb2823f0f980098ffe0158 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 3 Aug 2021 22:32:07 +0200 Subject: [PATCH 034/110] Add clang-format config for future use. --- .clang-format | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..f56f280b --- /dev/null +++ b/.clang-format @@ -0,0 +1,33 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +AlignAfterOpenBracket: Align +AlignEscapedNewlines: Left +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AlwaysBreakTemplateDeclarations: 'Yes' +AllowShortFunctionsOnASingleLine: Inline +AllowShortBlocksOnASingleLine: Always +AllowShortEnumsOnASingleLine: true +BreakConstructorInitializers: BeforeComma +ColumnLimit: '100' +ConstructorInitializerIndentWidth: '2' +IndentWidth: '2' +MaxEmptyLinesToKeep: '2' +PointerAlignment: Left +SortIncludes: 'true' +Standard: Auto +TabWidth: '2' +UseTab: Never +BreakBeforeBinaryOperators: NonAssignment +AlignConsecutiveAssignments: true +NamespaceIndentation: Inner +BreakBeforeBraces: Custom +BraceWrapping: + BeforeLambdaBody: false + AfterControlStatement: MultiLine + SplitEmptyFunction: false + SplitEmptyRecord: false + +... From 14f3c178bdaffe33d2b377678e18811dcf67ccb9 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 3 Aug 2021 22:32:46 +0200 Subject: [PATCH 035/110] Remove obsolete entries when updating translation files. --- cmake/modules/Translation.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/modules/Translation.cmake b/cmake/modules/Translation.cmake index ca199ca0..5eccc7c6 100644 --- a/cmake/modules/Translation.cmake +++ b/cmake/modules/Translation.cmake @@ -98,6 +98,7 @@ function(add_translation_update_task _prefix _input_dirs _output_dir _languages) ARGS ${_input_dirs} ARGS -locations relative ARGS -ts + ARGS -noobsolete ARGS ${_tsfiles_lupdate} WORKING_DIRECTORY ${_output_dir} COMMENT "Updating translations (${_prefix})..." From 4b8ceb11d4fb7bfbbdaeaa60c58604e7d9ef266f Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sun, 4 Jul 2021 15:11:50 +0530 Subject: [PATCH 036/110] Enable hold event on next and back button This commit configure the logitech spotlight device to send hold event on Next and Back button during device intialization (Fixes #71). Currently these events are not being utilized for any purpose (other than demo by moving the cursor on screen). --- src/device.cc | 42 +++++++++++++++++++++++++++-------- src/device.h | 12 +++++----- src/devicescan.cc | 1 + src/deviceswidget.cc | 7 ++++-- src/spotlight.cc | 53 +++++++++++++++++++++++++++++++++++++++++--- src/spotlight.h | 18 +++++++++++++++ 6 files changed, 114 insertions(+), 19 deletions(-) diff --git a/src/device.cc b/src/device.cc index 79cdb141..88f9950e 100644 --- a/src/device.cc +++ b/src/device.cc @@ -367,13 +367,14 @@ void SubDeviceConnection::pingSubDevice() } // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::setHIDProtocol(float version) { +void SubDeviceConnection::setHIDppProtocol(float version) { + // Inform user about the online status of device. if (version > 0) { - logDebug(hid) << path() << "is online with protocol version" << version ; + if (m_details.HIDppProtocolVer < 0) logInfo(hid) << "HID Device with path" << path() << tr("is now active with protocol version %1.").arg(version); } else { - logDebug(hid) << "HID Device with path" << path() << "got deactivated."; + if (m_details.HIDppProtocolVer > 0) logInfo(hid) << "HID Device with path" << path() << "got deactivated."; } - m_details.hidProtocolVer = version; + m_details.HIDppProtocolVer = version; } // ------------------------------------------------------------------------------------------------- @@ -405,6 +406,7 @@ void SubHidppConnection::initialize() sendData(data, sizeof(data));}); msgCount++; + // Now enable both software and wireless notification bit QTimer::singleShot(delay_ms*msgCount, this, [this](){ constexpr uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_SET_FEATURE, 0x00, 0x00, 0x09, 0x00}; sendData(data, sizeof(data));}); @@ -425,10 +427,16 @@ void SubHidppConnection::initialize() featureFlags |= DeviceFlag::ReportBattery; logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; } + if (m_featureSet->supportFeatureCode(FeatureCode::ReprogramControlsV4)) { + featureFlags |= DeviceFlags::NextHold; + featureFlags |= DeviceFlags::BackHold; + logDebug(hid) << "SubDevice" << path() << "can send next and back hold event."; + } } else { logWarn(hid) << "Loading FeatureSet for" << path() << "failed. Device might be inactive."; logInfo(hid) << "Press any button on device to activate it."; } + setFlags(featureFlags, true); setReadNotifierEnabled(true); // Reset spotlight device @@ -449,15 +457,31 @@ void SubHidppConnection::initialize() QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); msgCount++; } else if (m_details.busType == BusType::Bluetooth) { + // Bluetooth connection do not respond to ping. + // Hence, we are faking a ping response here. // Bluetooth connection mean HID++ v2.0+. // Setting version to 6.4: same as USB connection. - setHIDProtocol(6.4); + setHIDppProtocol(6.4); + emit receivedPingResponse(); } - setFlags(featureFlags, true); + // Enable Next and back button on hold functionality. + const auto rcID = m_featureSet->getFeatureID(FeatureCode::ReprogramControlsV4); + if (rcID) { + if (hasFlags(DeviceFlags::NextHold)) { + QTimer::singleShot(delay_ms*msgCount, this, [this, rcID](){ + const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcID, 0x3d, 0x00, 0xda, 0x33}; + sendData(data, sizeof(data));}); + msgCount++; + } - // Add other configuration to enable features in device - // like enabling on Next and back button on hold functionality. + if (hasFlags(DeviceFlags::BackHold)) { + QTimer::singleShot(delay_ms*msgCount, this, [this, rcID](){ + const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcID, 0x3d, 0x00, 0xdc, 0x33}; + sendData(data, sizeof(data));}); + msgCount++; + } + } } // ------------------------------------------------------------------------------------------------- @@ -580,7 +604,6 @@ std::shared_ptr SubHidppConnection::create(const DeviceScan: void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) { // TODO put in HIDPP - // TODO generalize features and protocol for proprietary device features like vibration // for not only the Spotlight device. // @@ -607,6 +630,7 @@ void SubHidppConnection::queryBatteryStatus() const uint8_t batteryCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; sendData(batteryCmd, sizeof(batteryCmd)); } + setWriteNotifierEnabled(false); } } diff --git a/src/device.h b/src/device.h index 589c0a98..a928425f 100644 --- a/src/device.h +++ b/src/device.h @@ -128,7 +128,9 @@ enum class DeviceFlag : uint32_t { Hidpp = 1 << 15, ///< Device supports hidpp requests Vibrate = 1 << 16, ///< Device supports vibrate commands - ReportBattery = 1 << 17, + ReportBattery = 1 << 17, ///< Device can report battery status + NextHold = 1 << 18, ///< Device can be configured to send 'Next Hold' event. + BackHold = 1 << 19, ///< Device can be configured to send 'Back Hold' event. }; ENUM(DeviceFlag, DeviceFlags) @@ -144,7 +146,7 @@ struct SubDeviceConnectionDetails { DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString phys; QString devicePath; - float hidProtocolVer = -1; // set after ping to HID sub-device; If positive then Hidraw device is online. + float HIDppProtocolVer = -1; // set after ping to HID sub-device; If positive then Hidraw device is online. }; // ------------------------------------------------------------------------------------------------- @@ -178,9 +180,9 @@ class SubDeviceConnection : public QObject, public async::Async 0); }; - void setHIDProtocol(float version); - float getHIDProtocol() const { return m_details.hidProtocolVer; }; + bool isOnline() const { return (m_details.HIDppProtocolVer > 0); }; + void setHIDppProtocol(float version); + float getHIDppProtocol() const { return m_details.HIDppProtocolVer; }; // Base implementation of generic write methods to the device does nothing. virtual ssize_t sendData(const QByteArray& msg); diff --git a/src/devicescan.cc b/src/devicescan.cc index 4782010f..6044a742 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -6,6 +6,7 @@ #include #include +#include // Function declaration to check for extra devices, definition in generated source bool isExtraDeviceSupported(quint16 vendorId, quint16 productId); diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 85021aee..0efa7a7c 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -155,8 +155,11 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) // report special flags set by program (like vibration and others) auto flagText = [](DeviceFlag f){ QStringList flagList; + if (!!(f & DeviceFlag::Hidpp)) flagList.push_back("HID++"); if (!!(f & DeviceFlag::Vibrate)) flagList.push_back("Vibration"); if (!!(f & DeviceFlag::ReportBattery)) flagList.push_back("Report_Battery"); + if (!!(f & DeviceFlag::NextHold)) flagList.push_back("Next_Hold"); + if (!!(f & DeviceFlag::BackHold)) flagList.push_back("Back_Hold"); return flagList; }; for (const auto& sd: dc->subDevices()) { @@ -215,8 +218,8 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) }); deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); - deviceDetails += tr("VendorId:\t%1\n").arg(logging::hexId(dc->deviceId().vendorId)); - deviceDetails += tr("ProductId:\t%1\n").arg(logging::hexId(dc->deviceId().productId)); + deviceDetails += tr("VendorId:\t%1\n").arg(hexId(dc->deviceId().vendorId)); + deviceDetails += tr("ProductId:\t%1\n").arg(hexId(dc->deviceId().productId)); deviceDetails += tr("Phys:\t\t%1\n").arg(dc->deviceId().phys); deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); diff --git a/src/spotlight.cc b/src/spotlight.cc index bc76001c..fa04e6ef 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -347,7 +348,7 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) if (readVal.at(2) == HIDPP::Bytes::SHORT_WIRELESS_NOTIFICATION_CODE) { // wireless notification from USB dongle auto connection_status = readVal.at(4) & (1<<6); // should be zero for successful connection if (connection_status) { // connection between USB dongle and spotlight device broke - connection.setHIDProtocol(-1); + connection.setHIDppProtocol(-1); } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. if (!connection.isOnline()) connection.initialize(); } @@ -360,7 +361,7 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) if (readVal.at(2) == rootID) { if (readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) { // response to ping auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; - connection.setHIDProtocol(protocolVer); + connection.setHIDppProtocol(protocolVer); if (connection.isOnline()) emit connection.receivedPingResponse(); } } @@ -377,7 +378,53 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) emit connection.receivedBatteryInfo(batteryData); } - // TODO: Process other packets + // Process reprogrammed keys : Next Hold and Back Hold + auto reprogrammedControlID = connection.getFeatureSet()->getFeatureID(FeatureCode::ReprogramControlsV4); + if (reprogrammedControlID && readVal.at(2) == reprogrammedControlID) // Button (for which hold events are on) related message. + { + auto eventCode = static_cast(readVal.at(3)); + auto buttonCode = static_cast(readVal.at(5)); + if (eventCode == 0x00) { // hold start events + switch (buttonCode) { + case 0xda: + logDebug(hid) << "Next Hold Event "; + m_holdButtonStatus.setButton(ActiveHoldButton::Next); + break; + case 0xdc: + logDebug(hid) << "Back Hold Event "; + m_holdButtonStatus.setButton(ActiveHoldButton::Back); + break; + case 0x00: + // hold event over. + logDebug(hid) << "Hold Event over."; + m_holdButtonStatus.reset(); + } + } + else if (eventCode == 0x10) { // mouse move event + // Mouse data is sent as 4 byte information starting at 4th index and ending at 7th. + // out of these 5th byte and 7th byte are x and y relative change, respectively. + // the forth byte show horizonal scroll towards right if rel value is -1 otherwise left scroll (0) + // the sixth byte show vertical scroll towards up if rel value is -1 otherwise down scroll (0) + auto byteToRel = [](int i){return ( (i<128) ? i : 256-i);}; // convert the byte to relative motion in x or y + int x = byteToRel(readVal.at(5)); + int y = byteToRel(readVal.at(7)); + //logInfo(device) << byteToRel(readVal.at(4)) <<", "<< x<<", "<< byteToRel(readVal.at(6)) <<", "<getFeatureID(FeatureCode::PresenterControl); diff --git a/src/spotlight.h b/src/spotlight.h index 1c25e6c5..51c65861 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -13,6 +13,23 @@ class QTimer; class Settings; class VirtualDevice; +// ----------------------------------------------------------------------------------------------- +enum class ActiveHoldButton : uint8_t { None, Next, Back }; + +// ----------------------------------------------------------------------------------------------- +struct HoldButtonStatus { + void setButton(ActiveHoldButton b){ _button = b; _numEvents=0; }; + auto getButton() const { return _button; } + int numEvents() const { return _numEvents; }; + void addEvent(){ _numEvents++; }; + void reset(){ setButton(ActiveHoldButton::None); }; + +private: + ActiveHoldButton _button = ActiveHoldButton::None; + unsigned long _numEvents = 0; +}; + + /// Class handling spotlight device connections and indicating if a device is sending /// sending mouse move events. class Spotlight : public QObject @@ -71,4 +88,5 @@ class Spotlight : public QObject bool m_spotActive = false; std::shared_ptr m_virtualDevice; Settings* m_settings = nullptr; + HoldButtonStatus m_holdButtonStatus; }; From 465eefdfecfccbd37167ce8947ed2d2eee15fd21 Mon Sep 17 00:00:00 2001 From: Jahn Date: Mon, 9 Aug 2021 22:48:40 +0200 Subject: [PATCH 037/110] Fix input mapping. Fixes #144 --- src/deviceinput.cc | 159 ++++++++++++++++++--------------------------- 1 file changed, 63 insertions(+), 96 deletions(-) diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 50927479..4ae5b6d5 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -6,8 +6,7 @@ #include "virtualdevice.h" #include -#include -#include +#include #include #include @@ -141,32 +140,13 @@ QDataStream& operator<<(QDataStream& s, const MappedAction& mia) { // ------------------------------------------------------------------------------------------------- namespace { - struct Next; - // Map of Key event and the next possible key events. - using SynKeyEventMap = std::map>; - using RefPair = SynKeyEventMap::value_type; - // Set of references to the next possible key event. - using RefSet = std::set; - - struct Next { + struct KeyEventItem { + KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} + const KeyEvent keyEvent; std::shared_ptr action; - RefSet next_events; + std::vector nextMap; }; - // Helper function - size_t maxSequenceLength(const InputMapConfig& config) - { - const auto max = std::max_element(config.cbegin(), config.cend(), - [](const auto& a, const auto& b){ - return a.first.size() < b.first.size(); - }); - - return ((max == config.cend()) ? 0 : max->first.size()); - } - - // Internal data structure for keeping track of key events and checking if a configured - // key event sequence was pressed. Needs to be completely reconstructed/reconfigured - // if the configuration changes. struct DeviceKeyMap { DeviceKeyMap(const InputMapConfig& config = {}) { reconfigure(config); } @@ -180,11 +160,12 @@ namespace { auto state() const { return m_pos; } void resetState(); void reconfigure(const InputMapConfig& config = {}); - bool hasConfig() const { return m_keymaps.size(); } + bool hasConfig() const { return m_rootItem.nextMap.size(); } private: - const RefPair* m_pos = nullptr; - std::vector m_keymaps; + std::list m_items; + KeyEventItem m_rootItem; + const KeyEventItem* m_pos = &m_rootItem; }; } @@ -192,35 +173,26 @@ namespace { DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], size_t num) { if (!hasConfig()) return Result::Miss; + if (!m_pos) return Result::Miss; - if (m_pos == nullptr) - { - const auto find_it = m_keymaps[0].find(KeyEvent(input_events, input_events + num)); - if (find_it == m_keymaps[0].cend()) return Result::Miss; - m_pos = &(*find_it); - } - else - { - if (!m_pos->second) return Result::Miss; + const auto ke = KeyEvent(KeyEvent(input_events, input_events + num)); + const auto& nextMap = m_pos->nextMap; + const auto find_it = std::find_if(nextMap.cbegin(), nextMap.cend(), + [&ke](KeyEventItem const* next) { + return next && ke == next->keyEvent; + }); - const auto ke = KeyEvent(KeyEvent(input_events, input_events + num)); - const auto& set = m_pos->second->next_events; - const auto find_it = std::find_if(set.cbegin(), set.cend(), [&ke](RefPair const* next_ptr) { - return ke == next_ptr->first; - }); + if (find_it == nextMap.cend()) return Result::Miss; - if (find_it == set.cend()) return Result::Miss; - - m_pos = (*find_it); - } + m_pos = (*find_it); // Last KeyEvent in possible sequence... - if (!m_pos->second || m_pos->second->next_events.empty()) { + if (m_pos->nextMap.empty()) { return Result::Hit; } // KeyEvent in Sequence has action attached, but there are other possible sequences... - if (m_pos->second->action && !m_pos->second->action->empty()) { + if (m_pos->action && !m_pos->action->empty()) { return Result::PartialHit; } @@ -230,52 +202,47 @@ DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], // ------------------------------------------------------------------------------------------------- void DeviceKeyMap::resetState() { - m_pos = nullptr; + m_pos = &m_rootItem; } // ------------------------------------------------------------------------------------------------- void DeviceKeyMap::reconfigure(const InputMapConfig& config) { - m_keymaps.resize(maxSequenceLength(config)); - // -- clear maps + state resetState(); - for (auto& synKeyEventMap : m_keymaps) { synKeyEventMap.clear(); } + m_rootItem.nextMap.clear(); + m_items.clear(); - // -- fill maps - for (const auto& item: config) + // -- fill keymaps + for (const auto& configItem : config) { - if (!item.second.action || item.second.action->empty()) continue; + KeyEventItem* previous = nullptr; + KeyEventItem* current = &m_rootItem; + const auto& kes = configItem.first; - const auto& kes = item.first; for (size_t i = 0; i < kes.size(); ++i) { - m_keymaps[i].emplace(kes[i], nullptr); - } - } - - // -- fill references - for (const auto& item: config) - { - if (!item.second.action || item.second.action->empty()) continue; - - const auto& kes = item.first; - for (size_t i = 0; i < kes.size(); ++i) - { - const auto r = m_keymaps[i].equal_range(kes[i]); - if (r.first == r.second) continue; - auto& refobj = r.first->second; - if (!refobj) { - refobj = std::make_unique(); + const auto& keyEvent = kes[i]; + const auto it = std::find_if(current->nextMap.cbegin(), current->nextMap.cend(), + [&keyEvent](const KeyEventItem* item) { + return (item && item->keyEvent == keyEvent); + }); + + if (it != current->nextMap.cend()) { + previous = current; + current = *it; } - - if (i == kes.size() - 1) { // last keyevent in seq - refobj->action = item.second.action; + else { + // Create new item if not found + m_items.emplace_back(KeyEventItem{keyEvent}); + previous = current; + current = &m_items.back(); + // link previous to current + previous->nextMap.push_back(current); } - else if (i+1 < m_keymaps.size()) // if not last keyevent in seq - { - const auto r = m_keymaps[i+1].equal_range(kes[i+1]); - if (r.first == r.second) continue; - refobj->next_events.emplace(&(*r.first)); + + // if last item in key event set + if (i == kes.size() - 1) { + current->action = configItem.second.action; } } } @@ -286,19 +253,19 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) NativeKeySequence::NativeKeySequence() = default; // ------------------------------------------------------------------------------------------------- -NativeKeySequence::NativeKeySequence(const std::vector& qtKeys, +NativeKeySequence::NativeKeySequence(const std::vector& qtKeys, std::vector&& nativeModifiers, KeyEventSequence&& kes) : m_keySequence(makeQKeySequence(qtKeys)) , m_nativeSequence(std::move(kes)) , m_nativeModifiers(std::move(nativeModifiers)) -{ +{ } // ------------------------------------------------------------------------------------------------- bool NativeKeySequence::operator==(const NativeKeySequence &other) const { - return m_keySequence == other.m_keySequence + return m_keySequence == other.m_keySequence && m_nativeSequence == other.m_nativeSequence && m_nativeModifiers == other.m_nativeModifiers; } @@ -306,7 +273,7 @@ bool NativeKeySequence::operator==(const NativeKeySequence &other) const // ------------------------------------------------------------------------------------------------- bool NativeKeySequence::operator!=(const NativeKeySequence &other) const { - return m_keySequence != other.m_keySequence + return m_keySequence != other.m_keySequence || m_nativeSequence != other.m_nativeSequence || m_nativeModifiers != other.m_nativeModifiers; } @@ -330,10 +297,10 @@ QString NativeKeySequence::toString() const { QString seqString; const size_t size = count(); - for (size_t i = 0; i < size; ++i) + for (size_t i = 0; i < size; ++i) { if (i > 0) seqString += QLatin1String(", "); - seqString += toString(m_keySequence[i], + seqString += toString(m_keySequence[i], (i < m_nativeModifiers.size()) ? m_nativeModifiers[i] : (uint16_t)Modifier::NoModifier); } @@ -343,7 +310,7 @@ QString NativeKeySequence::toString() const // ------------------------------------------------------------------------------------------------- QString NativeKeySequence::toString(int qtKey, uint16_t nativeModifiers) { - QString keyStr; + QString keyStr; if (qtKey == 0) // Special case for manually created Key Sequences { @@ -379,7 +346,7 @@ QString NativeKeySequence::toString(int qtKey, uint16_t nativeModifiers) if((qtKey & Qt::ControlModifier) == Qt::ControlModifier) { addKeyToString(keyStr, QLatin1String("Ctrl")); - } + } if((qtKey & Qt::AltModifier) == Qt::AltModifier) { addKeyToString(keyStr, QLatin1String("Alt")); @@ -406,10 +373,10 @@ QString NativeKeySequence::toString(const std::vector& qtKeys, { QString seqString; const auto size = qtKeys.size(); - for (size_t i = 0; i < size; ++i) + for (size_t i = 0; i < size; ++i) { if (i > 0) seqString += QLatin1String(", "); - seqString += toString(qtKeys[i], + seqString += toString(qtKeys[i], (i < nativeModifiers.size()) ? nativeModifiers[i] : (uint16_t)Modifier::NoModifier); } @@ -501,7 +468,7 @@ struct InputMapper::Impl QTimer* m_seqTimer = nullptr; DeviceKeyMap m_keymap; - std::pair m_lastState; + std::pair m_lastState; std::vector m_events; InputMapConfig m_config; bool m_recordingMode = false; @@ -559,9 +526,9 @@ void InputMapper::Impl::sequenceTimeout() else if (m_lastState.first == DeviceKeyMap::Result::PartialHit) { // Last input could have triggered an action, but we needed to wait for the timeout, since // other sequences could have been possible. - if (m_lastState.second->second) + if (m_lastState.second) { - execAction(m_lastState.second->second->action, DeviceKeyMap::Result::PartialHit); + execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); } else if (m_vdev && m_events.size()) { @@ -735,8 +702,8 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) impl->m_seqTimer->stop(); if (impl->m_vdev) { - if (impl->m_keymap.state()->second) { - impl->execAction(impl->m_keymap.state()->second->action, DeviceKeyMap::Result::Hit); + if (const auto pos = impl->m_keymap.state()) { + impl->execAction(pos->action, DeviceKeyMap::Result::Hit); } else { From 82ef5748d59d1e7d3646eec7adee6648bb18bc97 Mon Sep 17 00:00:00 2001 From: Jahn Date: Mon, 9 Aug 2021 22:48:40 +0200 Subject: [PATCH 038/110] Fix input mapping. Fixes #144 --- src/deviceinput.cc | 159 ++++++++++++++++++--------------------------- 1 file changed, 63 insertions(+), 96 deletions(-) diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 50927479..4ae5b6d5 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -6,8 +6,7 @@ #include "virtualdevice.h" #include -#include -#include +#include #include #include @@ -141,32 +140,13 @@ QDataStream& operator<<(QDataStream& s, const MappedAction& mia) { // ------------------------------------------------------------------------------------------------- namespace { - struct Next; - // Map of Key event and the next possible key events. - using SynKeyEventMap = std::map>; - using RefPair = SynKeyEventMap::value_type; - // Set of references to the next possible key event. - using RefSet = std::set; - - struct Next { + struct KeyEventItem { + KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} + const KeyEvent keyEvent; std::shared_ptr action; - RefSet next_events; + std::vector nextMap; }; - // Helper function - size_t maxSequenceLength(const InputMapConfig& config) - { - const auto max = std::max_element(config.cbegin(), config.cend(), - [](const auto& a, const auto& b){ - return a.first.size() < b.first.size(); - }); - - return ((max == config.cend()) ? 0 : max->first.size()); - } - - // Internal data structure for keeping track of key events and checking if a configured - // key event sequence was pressed. Needs to be completely reconstructed/reconfigured - // if the configuration changes. struct DeviceKeyMap { DeviceKeyMap(const InputMapConfig& config = {}) { reconfigure(config); } @@ -180,11 +160,12 @@ namespace { auto state() const { return m_pos; } void resetState(); void reconfigure(const InputMapConfig& config = {}); - bool hasConfig() const { return m_keymaps.size(); } + bool hasConfig() const { return m_rootItem.nextMap.size(); } private: - const RefPair* m_pos = nullptr; - std::vector m_keymaps; + std::list m_items; + KeyEventItem m_rootItem; + const KeyEventItem* m_pos = &m_rootItem; }; } @@ -192,35 +173,26 @@ namespace { DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], size_t num) { if (!hasConfig()) return Result::Miss; + if (!m_pos) return Result::Miss; - if (m_pos == nullptr) - { - const auto find_it = m_keymaps[0].find(KeyEvent(input_events, input_events + num)); - if (find_it == m_keymaps[0].cend()) return Result::Miss; - m_pos = &(*find_it); - } - else - { - if (!m_pos->second) return Result::Miss; + const auto ke = KeyEvent(KeyEvent(input_events, input_events + num)); + const auto& nextMap = m_pos->nextMap; + const auto find_it = std::find_if(nextMap.cbegin(), nextMap.cend(), + [&ke](KeyEventItem const* next) { + return next && ke == next->keyEvent; + }); - const auto ke = KeyEvent(KeyEvent(input_events, input_events + num)); - const auto& set = m_pos->second->next_events; - const auto find_it = std::find_if(set.cbegin(), set.cend(), [&ke](RefPair const* next_ptr) { - return ke == next_ptr->first; - }); + if (find_it == nextMap.cend()) return Result::Miss; - if (find_it == set.cend()) return Result::Miss; - - m_pos = (*find_it); - } + m_pos = (*find_it); // Last KeyEvent in possible sequence... - if (!m_pos->second || m_pos->second->next_events.empty()) { + if (m_pos->nextMap.empty()) { return Result::Hit; } // KeyEvent in Sequence has action attached, but there are other possible sequences... - if (m_pos->second->action && !m_pos->second->action->empty()) { + if (m_pos->action && !m_pos->action->empty()) { return Result::PartialHit; } @@ -230,52 +202,47 @@ DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], // ------------------------------------------------------------------------------------------------- void DeviceKeyMap::resetState() { - m_pos = nullptr; + m_pos = &m_rootItem; } // ------------------------------------------------------------------------------------------------- void DeviceKeyMap::reconfigure(const InputMapConfig& config) { - m_keymaps.resize(maxSequenceLength(config)); - // -- clear maps + state resetState(); - for (auto& synKeyEventMap : m_keymaps) { synKeyEventMap.clear(); } + m_rootItem.nextMap.clear(); + m_items.clear(); - // -- fill maps - for (const auto& item: config) + // -- fill keymaps + for (const auto& configItem : config) { - if (!item.second.action || item.second.action->empty()) continue; + KeyEventItem* previous = nullptr; + KeyEventItem* current = &m_rootItem; + const auto& kes = configItem.first; - const auto& kes = item.first; for (size_t i = 0; i < kes.size(); ++i) { - m_keymaps[i].emplace(kes[i], nullptr); - } - } - - // -- fill references - for (const auto& item: config) - { - if (!item.second.action || item.second.action->empty()) continue; - - const auto& kes = item.first; - for (size_t i = 0; i < kes.size(); ++i) - { - const auto r = m_keymaps[i].equal_range(kes[i]); - if (r.first == r.second) continue; - auto& refobj = r.first->second; - if (!refobj) { - refobj = std::make_unique(); + const auto& keyEvent = kes[i]; + const auto it = std::find_if(current->nextMap.cbegin(), current->nextMap.cend(), + [&keyEvent](const KeyEventItem* item) { + return (item && item->keyEvent == keyEvent); + }); + + if (it != current->nextMap.cend()) { + previous = current; + current = *it; } - - if (i == kes.size() - 1) { // last keyevent in seq - refobj->action = item.second.action; + else { + // Create new item if not found + m_items.emplace_back(KeyEventItem{keyEvent}); + previous = current; + current = &m_items.back(); + // link previous to current + previous->nextMap.push_back(current); } - else if (i+1 < m_keymaps.size()) // if not last keyevent in seq - { - const auto r = m_keymaps[i+1].equal_range(kes[i+1]); - if (r.first == r.second) continue; - refobj->next_events.emplace(&(*r.first)); + + // if last item in key event set + if (i == kes.size() - 1) { + current->action = configItem.second.action; } } } @@ -286,19 +253,19 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) NativeKeySequence::NativeKeySequence() = default; // ------------------------------------------------------------------------------------------------- -NativeKeySequence::NativeKeySequence(const std::vector& qtKeys, +NativeKeySequence::NativeKeySequence(const std::vector& qtKeys, std::vector&& nativeModifiers, KeyEventSequence&& kes) : m_keySequence(makeQKeySequence(qtKeys)) , m_nativeSequence(std::move(kes)) , m_nativeModifiers(std::move(nativeModifiers)) -{ +{ } // ------------------------------------------------------------------------------------------------- bool NativeKeySequence::operator==(const NativeKeySequence &other) const { - return m_keySequence == other.m_keySequence + return m_keySequence == other.m_keySequence && m_nativeSequence == other.m_nativeSequence && m_nativeModifiers == other.m_nativeModifiers; } @@ -306,7 +273,7 @@ bool NativeKeySequence::operator==(const NativeKeySequence &other) const // ------------------------------------------------------------------------------------------------- bool NativeKeySequence::operator!=(const NativeKeySequence &other) const { - return m_keySequence != other.m_keySequence + return m_keySequence != other.m_keySequence || m_nativeSequence != other.m_nativeSequence || m_nativeModifiers != other.m_nativeModifiers; } @@ -330,10 +297,10 @@ QString NativeKeySequence::toString() const { QString seqString; const size_t size = count(); - for (size_t i = 0; i < size; ++i) + for (size_t i = 0; i < size; ++i) { if (i > 0) seqString += QLatin1String(", "); - seqString += toString(m_keySequence[i], + seqString += toString(m_keySequence[i], (i < m_nativeModifiers.size()) ? m_nativeModifiers[i] : (uint16_t)Modifier::NoModifier); } @@ -343,7 +310,7 @@ QString NativeKeySequence::toString() const // ------------------------------------------------------------------------------------------------- QString NativeKeySequence::toString(int qtKey, uint16_t nativeModifiers) { - QString keyStr; + QString keyStr; if (qtKey == 0) // Special case for manually created Key Sequences { @@ -379,7 +346,7 @@ QString NativeKeySequence::toString(int qtKey, uint16_t nativeModifiers) if((qtKey & Qt::ControlModifier) == Qt::ControlModifier) { addKeyToString(keyStr, QLatin1String("Ctrl")); - } + } if((qtKey & Qt::AltModifier) == Qt::AltModifier) { addKeyToString(keyStr, QLatin1String("Alt")); @@ -406,10 +373,10 @@ QString NativeKeySequence::toString(const std::vector& qtKeys, { QString seqString; const auto size = qtKeys.size(); - for (size_t i = 0; i < size; ++i) + for (size_t i = 0; i < size; ++i) { if (i > 0) seqString += QLatin1String(", "); - seqString += toString(qtKeys[i], + seqString += toString(qtKeys[i], (i < nativeModifiers.size()) ? nativeModifiers[i] : (uint16_t)Modifier::NoModifier); } @@ -501,7 +468,7 @@ struct InputMapper::Impl QTimer* m_seqTimer = nullptr; DeviceKeyMap m_keymap; - std::pair m_lastState; + std::pair m_lastState; std::vector m_events; InputMapConfig m_config; bool m_recordingMode = false; @@ -559,9 +526,9 @@ void InputMapper::Impl::sequenceTimeout() else if (m_lastState.first == DeviceKeyMap::Result::PartialHit) { // Last input could have triggered an action, but we needed to wait for the timeout, since // other sequences could have been possible. - if (m_lastState.second->second) + if (m_lastState.second) { - execAction(m_lastState.second->second->action, DeviceKeyMap::Result::PartialHit); + execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); } else if (m_vdev && m_events.size()) { @@ -735,8 +702,8 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) impl->m_seqTimer->stop(); if (impl->m_vdev) { - if (impl->m_keymap.state()->second) { - impl->execAction(impl->m_keymap.state()->second->action, DeviceKeyMap::Result::Hit); + if (const auto pos = impl->m_keymap.state()) { + impl->execAction(pos->action, DeviceKeyMap::Result::Hit); } else { From 0ca20a361a4616a63adff2ccb29c6a9949af7a26 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sun, 8 Aug 2021 14:21:04 +0530 Subject: [PATCH 039/110] Added support for repeated Action on Next/Back Hold Three repeated actions are added (Sroll Vertical, Scroll Horizontal and Volume Control). These actions can only be mapped to Next Hold or Back Hold Events. Pre-existing actions like Key Sequence, Cycle Preset, Toggle Spotlight are marked non-repeated as their purpose is fulfilled by calling them once only. However, repeated actions (like Scrolling and Controlling Volume) need repeated call with data sent from Spotlight device (during button hold events) to fulfill their purpose. --- README.md | 22 +++++++- src/actiondelegate.cc | 99 ++++++++++++++++++++++++++++++++--- src/device.cc | 18 ++++++- src/deviceinput.cc | 67 +++++++++++++----------- src/deviceinput.h | 94 ++++++++++++++++++++++++++++++++++ src/deviceswidget.cc | 40 ++++++++------- src/inputmapconfig.cc | 37 +++++++++----- src/inputmapconfig.h | 1 + src/inputseqedit.cc | 81 ++++++++++++++++++++++++----- src/inputseqedit.h | 3 ++ src/spotlight.cc | 116 +++++++++++++++++++++++++++++++++++------- src/spotlight.h | 25 ++++++--- 12 files changed, 493 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 5bc076d5..87762d50 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,30 @@ For more details: Have a look at the source code ;) Button mapping works by **grabbing** all device events of connected devices and forwarding them to a virtual _'uinput'_ device if not configured differently by the button mapping configuration. If a mapped configuration for -a button exists, _Projecteur_ will inject the mapped keyboard events instead. +a button exists, _Projecteur_ will inject the mapped action instead. (You can still disable device grabbing with the `--disable-uinput` command line option - button mapping will be disabled then.) +Multiple mapped actions like Key Sequence, Cycle Preset etc. exist. The Key +Sequence action is particularly powerful as it can emit any user-defined +keystroke. These keystrokes can invoke shortcut in presentation software +being used. Some relevant shortcuts for presentations (support to these +shortcuts may vary between presentation softwares) are: + + * b or . : Toggle blank screen + * w or , : Toggle white screen + * F5 : Start presentation from the first slide + * Shift + F5 : Start presentation from the current slide + +#### Hold Button Mapping for Logitech Spotlight + +Logitech Spotlight can send Hold event for Next and Back buttons as HID++ +messages. For mapping those inputs, please ensure that the device is active +by pressing any button, then go to Input Mapping tab under Devices tab in +Preferences dialog box and right click in first column (Input Sequence) for +any entry. Additional mapped actions (like Scrolling, volume control) can +be selected for such hold events. + ## Download The latest binary packages for some Linux distributions are available for download on cloudsmith. diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index ea8ecfd4..393a3b45 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -68,6 +68,54 @@ namespace { return QSize(100,16); } } + + namespace scrollhorizontal { + // --------------------------------------------------------------------------------------------- + void paint(QPainter* p, const QStyleOptionViewItem& option, const ScrollHorizontalAction* /*action*/) + { + const auto& fm = option.fontMetrics; + const int xPos = (option.rect.height()-fm.height()) / 2; + NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Scroll Horizontal")); + } + + // --------------------------------------------------------------------------------------------- + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollHorizontalAction* /*action*/) + { + return QSize(100,16); + } + } + + namespace scrollvertical { + // --------------------------------------------------------------------------------------------- + void paint(QPainter* p, const QStyleOptionViewItem& option, const ScrollVerticalAction* /*action*/) + { + const auto& fm = option.fontMetrics; + const int xPos = (option.rect.height()-fm.height()) / 2; + NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Scroll Vertical")); + } + + // --------------------------------------------------------------------------------------------- + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollVerticalAction* /*action*/) + { + return QSize(100,16); + } + } + + namespace volumecontrol { + // --------------------------------------------------------------------------------------------- + void paint(QPainter* p, const QStyleOptionViewItem& option, const VolumeControlAction* /*action*/) + { + const auto& fm = option.fontMetrics; + const int xPos = (option.rect.height()-fm.height()) / 2; + NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Volume Control")); + } + + // --------------------------------------------------------------------------------------------- + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const VolumeControlAction* /*action*/) + { + return QSize(100,16); + } + } } // end anonymous namespace // ------------------------------------------------------------------------------------------------- @@ -94,6 +142,15 @@ void ActionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option case Action::Type::ToggleSpotlight: togglespotlight::paint(painter, option, static_cast(item.action.get())); break; + case Action::Type::ScrollHorizontal: + scrollhorizontal::paint(painter, option, static_cast(item.action.get())); + break; + case Action::Type::ScrollVertical: + scrollvertical::paint(painter, option, static_cast(item.action.get())); + break; + case Action::Type::VolumeControl: + volumecontrol::paint(painter, option, static_cast(item.action.get())); + break; } if (option.state & QStyle::State_HasFocus) { @@ -117,6 +174,13 @@ QSize ActionDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelInde return cyclepresets::sizeHint(opt, static_cast(item.action.get())); case Action::Type::ToggleSpotlight: return togglespotlight::sizeHint(opt, static_cast(item.action.get())); + case Action::Type::ScrollHorizontal: + return scrollhorizontal::sizeHint(opt, static_cast(item.action.get())); + case Action::Type::ScrollVertical: + return scrollvertical::sizeHint(opt, static_cast(item.action.get())); + case Action::Type::VolumeControl: + return volumecontrol::sizeHint(opt, static_cast(item.action.get())); + } return QStyledItemDelegate::sizeHint(opt, index); @@ -136,6 +200,12 @@ QWidget* ActionDelegate::createEditor(QWidget* parent, const Action* action) con break; case Action::Type::ToggleSpotlight: // None for now... break; + case Action::Type::ScrollHorizontal: // None for now... + break; + case Action::Type::ScrollVertical: // None for now... + break; + case Action::Type::VolumeControl: // None for now... + break; } return nullptr; } @@ -254,6 +324,9 @@ void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op case Action::Type::KeySequence: return Font::Icon::keyboard_4; case Action::Type::CyclePresets: return Font::Icon::connection_8; case Action::Type::ToggleSpotlight: return Font::Icon::power_on_off_11; + case Action::Type::ScrollHorizontal: return Font::Icon::power_on_off_11; + case Action::Type::ScrollVertical: return Font::Icon::power_on_off_11; + case Action::Type::VolumeControl: return Font::Icon::power_on_off_11; } return 0; }(); @@ -274,17 +347,27 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* const auto& item = model->configData(index); if (!item.action) return; + const bool showRepeatedActions = std::any_of( + ReservedKeyEventSequence::HoldButtonsInfo.cbegin(), + ReservedKeyEventSequence::HoldButtonsInfo.cend(), + [item](const auto& button){ + return (item.deviceSequence == button.keqEventSeq);}); + struct actionEntry { Action::Type type; QChar symbol; QString text; + bool isRepeated; QIcon icon = {}; }; static std::vector items { - {Action::Type::KeySequence, Font::Icon::keyboard_4, tr("Key Sequence")}, - {Action::Type::CyclePresets, Font::Icon::connection_8, tr("Cycle Presets")}, - {Action::Type::ToggleSpotlight, Font::Icon::power_on_off_11, tr("Toggle Spotlight")}, + {Action::Type::KeySequence, Font::Icon::keyboard_4, tr("Key Sequence"), KeySequenceAction().isRepeated()}, + {Action::Type::CyclePresets, Font::Icon::connection_8, tr("Cycle Presets"), CyclePresetsAction().isRepeated()}, + {Action::Type::ToggleSpotlight, Font::Icon::power_on_off_11, tr("Toggle Spotlight"), ToggleSpotlightAction().isRepeated()}, + {Action::Type::ScrollHorizontal, Font::Icon::power_on_off_11, tr("Scroll Horizontal"), ScrollHorizontalAction().isRepeated()}, + {Action::Type::ScrollVertical, Font::Icon::power_on_off_11, tr("Scroll Vertical"), ScrollVerticalAction().isRepeated()}, + {Action::Type::VolumeControl, Font::Icon::power_on_off_11, tr("Volume Control"), VolumeControlAction().isRepeated()}, }; static bool initIcons = []() @@ -310,10 +393,12 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* QMenu* menu = new QMenu(parent); for (const auto& entry : items) { - const auto qaction = menu->addAction(entry.icon, entry.text); - connect(qaction, &QAction::triggered, this, [model, index, type=entry.type](){ - model->setItemActionType(index, type); - }); + if (!entry.isRepeated || (entry.isRepeated && showRepeatedActions)) { + const auto qaction = menu->addAction(entry.icon, entry.text); + connect(qaction, &QAction::triggered, this, [model, index, type=entry.type](){ + model->setItemActionType(index, type); + }); + }; } menu->exec(globalPos); diff --git a/src/device.cc b/src/device.cc index 88f9950e..d9c9cacf 100644 --- a/src/device.cc +++ b/src/device.cc @@ -151,7 +151,15 @@ void DeviceConnection::queryBatteryStatus() // ------------------------------------------------------------------------------------------------- void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) { - if (false) //if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus) && batteryData.length() == 3) + const bool hasBattery = std::any_of(m_subDeviceConnections.cbegin(), m_subDeviceConnections.cend(), + [](const auto& sd) + { + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + sd.second->hasFlags(DeviceFlag::ReportBattery)); + }); + + if (hasBattery && batteryData.length() == 3) { // Battery percent is only meaningful when battery is discharging. However, save them anyway. m_batteryInfo.currentLevel = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0): 100); @@ -352,6 +360,9 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca auto connection = std::make_shared(Token{}, sd, dc.deviceId()); connection->createSocketNotifiers(devfd); + + connection->m_inputMapper = dc.inputMapper(); + connection->m_details.phys = sd.phys; return connection; } @@ -428,8 +439,12 @@ void SubHidppConnection::initialize() logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; } if (m_featureSet->supportFeatureCode(FeatureCode::ReprogramControlsV4)) { + auto& reservedInputs = m_inputMapper->getReservedInputs(); + reservedInputs.clear(); featureFlags |= DeviceFlags::NextHold; featureFlags |= DeviceFlags::BackHold; + reservedInputs.emplace_back(ReservedKeyEventSequence::NextHoldInfo); + reservedInputs.emplace_back(ReservedKeyEventSequence::BackHoldInfo); logDebug(hid) << "SubDevice" << path() << "can send next and back hold event."; } } else { @@ -596,6 +611,7 @@ std::shared_ptr SubHidppConnection::create(const DeviceScan: } connection->createSocketNotifiers(devfd); + connection->m_inputMapper = dc.inputMapper(); connection->postTask([c=&*connection](){ c->initialize(); }); return connection; } diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 4ae5b6d5..90201c8a 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -106,6 +106,15 @@ QDataStream& operator>>(QDataStream& s, MappedAction& mia) { case Action::Type::ToggleSpotlight: mia.action = std::make_shared(); return mia.action->load(s); + case Action::Type::ScrollHorizontal: + mia.action = std::make_shared(); + return mia.action->load(s); + case Action::Type::ScrollVertical: + mia.action = std::make_shared(); + return mia.action->load(s); + case Action::Type::VolumeControl: + mia.action = std::make_shared(); + return mia.action->load(s); } return s; } @@ -127,6 +136,15 @@ bool MappedAction::operator==(const MappedAction& o) const case Action::Type::ToggleSpotlight: return (*static_cast(action.get())) == (*static_cast(o.action.get())); + case Action::Type::ScrollHorizontal: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); + case Action::Type::ScrollVertical: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); + case Action::Type::VolumeControl: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); } return false; @@ -460,7 +478,6 @@ struct InputMapper::Impl void sequenceTimeout(); void resetState(); void record(const struct input_event input_events[], size_t num); - void emitNativeKeySequence(const NativeKeySequence& ks); void execAction(const std::shared_ptr& action, DeviceKeyMap::Result r); InputMapper* m_parent = nullptr; @@ -493,16 +510,7 @@ void InputMapper::Impl::execAction(const std::shared_ptr& action, Device logDebug(input) << "Input map action, type = " << int(action->type()) << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit); - if (action->type() == Action::Type::KeySequence) - { - const auto keySequenceAction = static_cast(action.get()); - logDebug(input) << "Emitting Key Sequence:" << keySequenceAction->keySequence.toString(); - emitNativeKeySequence(keySequenceAction->keySequence); - } - else - { - emit m_parent->actionMapped(action); - } + emit m_parent->actionMapped(action); } // ------------------------------------------------------------------------------------------------- @@ -546,23 +554,6 @@ void InputMapper::Impl::resetState() m_events.resize(0); } -// ------------------------------------------------------------------------------------------------- -void InputMapper::Impl::emitNativeKeySequence(const NativeKeySequence& ks) -{ - if (!m_vdev) return; - - std::vector events; - events.reserve(5); // up to 3 modifier keys + 1 key + 1 syn event - for (const auto& ke : ks.nativeSequence()) - { - for (const auto& ie : ke) - events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); - - m_vdev->emitEvents(events); - events.resize(0); - } -} - // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::record(const struct input_event input_events[], size_t num) { @@ -724,6 +715,24 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) } } +// ------------------------------------------------------------------------------------------------- +void InputMapper::addEvents(const KeyEvent key_event) +{ + if (key_event.empty()) addEvents({}, 0); + + auto to_input_event = [](DeviceInputEvent de){ + struct input_event ie = {{}, de.type, de.code, de.value}; + return ie; + }; + + std::vector events; + for (size_t i=0; i < key_event.size(); i++) + { + events.emplace_back(to_input_event(key_event[i])); + } + addEvents(events.data(), events.size()); +} + // ------------------------------------------------------------------------------------------------- void InputMapper::resetState() { @@ -757,5 +766,3 @@ const InputMapConfig& InputMapper::configuration() const { return impl->m_config; } - - diff --git a/src/deviceinput.h b/src/deviceinput.h index b271ad14..553c6575 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -1,6 +1,8 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once +#include + #include #include @@ -73,6 +75,42 @@ QDataStream& operator>>(QDataStream& s, std::vector& container) QDebug operator<<(QDebug debug, const DeviceInputEvent &ie); QDebug operator<<(QDebug debug, const KeyEvent &ke); +// ------------------------------------------------------------------------------------------------- +// Some inputs from Logitech Spotlight device (like Next Hold and Back Hold events) are not valid +// input event (input_event in linux/input.h) in conventional sense. Rather they are communicated +// via HID++ messages from the device. To use to InputMapper architechture in that case we need to +// reserve some KeyEventSequence for such events. These KeyEventSequence should be designed in +// such a way that they cannot interfare with other valid input events from the device. +namespace ReservedKeyEventSequence { + const auto genKeyEventsWithoutSYN = [](std::vector codes){ + KeyEventSequence ks; + for (auto code: codes) + { + KeyEvent pressed; KeyEvent released; + pressed.emplace_back(EV_KEY, code, 1); + released.emplace_back(EV_KEY, code, 0); + ks.emplace_back(std::move(pressed)); + ks.emplace_back(std::move(released)); + } + return ks; + }; + + struct ReservedKeyEventSeqInfo {QString name; + KeyEventSequence keqEventSeq = {}; + }; + + // Use four key codes for Next and Back Hold event + const ReservedKeyEventSeqInfo NextHoldInfo = {"Next Hold", + genKeyEventsWithoutSYN({KEY_H, KEY_N, KEY_X, KEY_T})}; //HNXT: Reserved for Next Hold event + const ReservedKeyEventSeqInfo BackHoldInfo = {"Back Hold", + genKeyEventsWithoutSYN({KEY_H, KEY_B, KEY_C, KEY_K})}; //HBCK: Reserved for Back Hold event + + const std::array HoldButtonsInfo {{NextHoldInfo, BackHoldInfo}}; + + // Currently HoldButtonsInfo are the only reserved keys. May change in the future. + const auto ReservedKeyEvents = HoldButtonsInfo; +} + // ------------------------------------------------------------------------------------------------- class NativeKeySequence { @@ -143,6 +181,9 @@ struct Action KeySequence = 1, CyclePresets = 2, ToggleSpotlight = 3, + ScrollHorizontal = 11, + ScrollVertical = 12, + VolumeControl = 13, }; virtual ~Action() = default; @@ -151,6 +192,7 @@ struct Action virtual QDataStream& save(QDataStream&) const = 0; virtual QDataStream& load(QDataStream&) = 0; virtual bool empty() const = 0; + virtual bool isRepeated() const = 0; }; // ------------------------------------------------------------------------------------------------- @@ -162,6 +204,7 @@ struct KeySequenceAction : public Action QDataStream& save(QDataStream& s) const override { return s << keySequence; } QDataStream& load(QDataStream& s) override { return s >> keySequence; } bool empty() const override { return keySequence.empty(); } + bool isRepeated() const override { return false; } bool operator==(const KeySequenceAction& o) const { return keySequence == o.keySequence; } NativeKeySequence keySequence; @@ -174,6 +217,7 @@ struct CyclePresetsAction : public Action QDataStream& save(QDataStream& s) const override { return s << placeholder; } QDataStream& load(QDataStream& s) override { return s >> placeholder; } bool empty() const override { return false; } + bool isRepeated() const override { return false; } bool operator==(const CyclePresetsAction&) const { return true; } bool placeholder = false; }; @@ -185,10 +229,53 @@ struct ToggleSpotlightAction : public Action QDataStream& save(QDataStream& s) const override { return s << placeholder; } QDataStream& load(QDataStream& s) override { return s >> placeholder; } bool empty() const override { return false; } + bool isRepeated() const override { return false; } bool operator==(const ToggleSpotlightAction&) const { return true; } bool placeholder = false; }; +// ------------------------------------------------------------------------------------------------- +struct ScrollHorizontalAction : public Action +{ + Type type() const override { return Type::ScrollHorizontal; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool isRepeated() const override { return true; } + bool operator==(const ScrollHorizontalAction&) const { return true; } + bool placeholder = false; + + int param = 0; +}; + +// ------------------------------------------------------------------------------------------------- +struct ScrollVerticalAction : public Action +{ + Type type() const override { return Type::ScrollVertical; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool isRepeated() const override { return true; } + bool operator==(const ScrollVerticalAction&) const { return true; } + bool placeholder = false; + + int param = 0; +}; + +// ------------------------------------------------------------------------------------------------- +struct VolumeControlAction : public Action +{ + Type type() const override { return Type::VolumeControl; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool isRepeated() const override { return true; } + bool operator==(const VolumeControlAction&) const { return true; } + bool placeholder = false; + + int param = 0; +}; + // ------------------------------------------------------------------------------------------------- struct MappedAction { @@ -203,6 +290,9 @@ QDataStream& operator<<(QDataStream& s, const MappedAction& mia); // ------------------------------------------------------------------------------------------------- class InputMapConfig : public std::map{}; +// ------------------------------------------------------------------------------------------------- +using ReservedInput = std::vector; + // ------------------------------------------------------------------------------------------------- class InputMapper : public QObject { @@ -216,6 +306,7 @@ class InputMapper : public QObject // input_events = complete sequence including SYN event void addEvents(const struct input_event input_events[], size_t num); + void addEvents(const KeyEvent key_events); bool recordingMode() const; void setRecordingMode(bool recording); @@ -223,6 +314,8 @@ class InputMapper : public QObject int keyEventInterval() const; void setKeyEventInterval(int interval); + auto& getReservedInputs() { return reservedInputs; } + std::shared_ptr virtualDevice() const; bool hasVirtualDevice() const; @@ -244,4 +337,5 @@ class InputMapper : public QObject private: struct Impl; std::unique_ptr impl; + ReservedInput reservedInputs; }; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 0efa7a7c..302e8839 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -188,13 +188,24 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) }; auto sDevices = dc->subDevices(); - auto batteryInfoText = [dc, batteryStatusText, sDevices](){ - const bool isOnline = std::any_of(sDevices.cbegin(), sDevices.cend(), - [](const auto& sd){ - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - sd.second->isOnline());}); - if (isOnline) { + const bool isOnline = std::any_of(sDevices.cbegin(), sDevices.cend(), + [](const auto& sd){ + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + sd.second->isOnline());}); + const bool hasHIDPP = std::any_of(sDevices.cbegin(), sDevices.cend(), [](const auto& sd) + { + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + sd.second->hasFlags(DeviceFlag::Hidpp)); + }); + const bool hasBattery = std::any_of(sDevices.cbegin(), sDevices.cend(), [](const auto& sd) + { + return (sd.second->type() == ConnectionType::Hidraw && + sd.second->mode() == ConnectionMode::ReadWrite && + sd.second->hasFlags(DeviceFlag::ReportBattery)); + }); + auto batteryInfoText = [dc, batteryStatusText](){ auto batteryInfo= dc->getBatteryInfo(); // Only show battery percent while discharging. // Other cases, device do not report battery percentage correctly. @@ -206,16 +217,7 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) } else { return tr("%3").arg(batteryStatusText(batteryInfo.status)); } - } else { - return tr("Device not active. Press any key on device to update."); - } }; - const bool hasBattery = std::any_of(sDevices.cbegin(), sDevices.cend(), [](const auto& sd) - { - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - sd.second->hasFlags(DeviceFlag::ReportBattery)); - }); deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); deviceDetails += tr("VendorId:\t%1\n").arg(hexId(dc->deviceId().vendorId)); @@ -223,8 +225,8 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) deviceDetails += tr("Phys:\t\t%1\n").arg(dc->deviceId().phys); deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); - if (hasBattery) deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); - + if (hasBattery && isOnline) deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); + if (hasHIDPP && !isOnline) deviceDetails += tr("\n\t Device not active. Press any key on device to update.\n"); return deviceDetails; }; @@ -250,7 +252,7 @@ QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) connect(this, &DevicesWidget::currentDeviceChanged, this, [this, spotlight](){updateDeviceDetails(spotlight);}); connect(spotlight, &Spotlight::deviceActivated, this, - [this, spotlight](const DeviceId& d){if (d==currentDeviceId()){ updateDeviceDetails(spotlight);};}); + [this, spotlight](const DeviceId& d){if (d==currentDeviceId()) updateDeviceDetails(spotlight);}); layout->addWidget(m_deviceDetailsTextEdit); return diWidget; diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index 29effdb1..500ea0a2 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -204,6 +204,15 @@ void InputMapConfigModel::setItemActionType(const QModelIndex& idx, Action::Type case Action::Type::ToggleSpotlight: item.action = std::make_shared(); break; + case Action::Type::ScrollHorizontal: + item.action = std::make_shared(); + break; + case Action::Type::ScrollVertical: + item.action = std::make_shared(); + break; + case Action::Type::VolumeControl: + item.action = std::make_shared(); + break; } configureInputMapper(); @@ -275,8 +284,8 @@ void InputMapConfigModel::updateDuplicates() // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- InputMapConfigView::InputMapConfigView(QWidget* parent) - : QTableView(parent) - , m_actionTypeDelegate(new ActionTypeDelegate(this)) + : QTableView(parent), + m_actionTypeDelegate(new ActionTypeDelegate(this)) { verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); @@ -296,21 +305,25 @@ InputMapConfigView::InputMapConfigView(QWidget* parent) setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, - [this, actionDelegate](const QPoint& pos) + [this, imSeqDelegate, actionDelegate](const QPoint& pos) { const auto idx = indexAt(pos); if (!idx.isValid()) return; - if (idx.column() == InputMapConfigModel::ActionCol) + switch(idx.column()) { - actionDelegate->actionContextMenu(this, qobject_cast(model()), - idx, this->viewport()->mapToGlobal(pos)); - } - else if (idx.column() == InputMapConfigModel::ActionTypeCol) - { - m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), - idx, this->viewport()->mapToGlobal(pos)); - } + case InputMapConfigModel::InputSeqCol: + imSeqDelegate->inputSeqContextMenu(this, qobject_cast(model()), + idx, this->viewport()->mapToGlobal(pos)); + break; + case InputMapConfigModel::ActionTypeCol: + m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), + idx, this->viewport()->mapToGlobal(pos)); + break; + case InputMapConfigModel::ActionCol: + actionDelegate->actionContextMenu(this, qobject_cast(model()), + idx, this->viewport()->mapToGlobal(pos)); + }; }); connect(this, &QTableView::doubleClicked, this, [this](const QModelIndex& idx) diff --git a/src/inputmapconfig.h b/src/inputmapconfig.h index c2639337..17e25985 100644 --- a/src/inputmapconfig.h +++ b/src/inputmapconfig.h @@ -9,6 +9,7 @@ // ------------------------------------------------------------------------------------------------- class ActionTypeDelegate; +class InputSeqDelegate; // ------------------------------------------------------------------------------------------------- struct InputMapModelItem { diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 1fa8c651..45444ebf 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -47,7 +47,7 @@ namespace { // TODO some devices (e.g. August WP 200) have buttons that send a key combination // (modifiers + key) - this is ignored completely right now. const auto text = QString("[%1%2%3") - .arg(ke.back().code, 0, 16) + .arg(ke.back().code != SYN_REPORT ? ke.back().code : ke.front().code, 0, 16) .arg(buttonTap ? pressChar : ke.back().value ? pressChar : releaseChar) .arg(buttonTap ? "" : "]"); @@ -88,7 +88,7 @@ namespace { { if (!drawEmptyPlaceholder) { return 0; } return InputSeqEdit::drawEmptyIndicator(startX, p, option); - } + } int sequenceWidth = 0; const int paddingX = static_cast(QStaticText(" ").size().width()); @@ -112,6 +112,28 @@ namespace { return sequenceWidth; } + + // ----------------------------------------------------------------------------------------------- + int drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text, bool textDisabled) + { + const auto r = QRect(QPoint(startX + option.rect.left(), option.rect.top()), + option.rect.bottomRight()); + + p.save(); + if (textDisabled) + { + p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); + } + else + { + p.setPen(option.palette.color(QPalette::Text)); + } + QRect br; + p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); + p.restore(); + + return br.width(); + } } // ------------------------------------------------------------------------------------------------- @@ -192,7 +214,8 @@ void InputSeqEdit::paintEvent(QPaintEvent*) xPos += drawKeyEventSequence(xPos, p, option, m_recordedSequence, false); } } - else { + else + { xPos += drawKeyEventSequence(xPos, p, option, m_inputSequence); } } @@ -342,16 +365,7 @@ int InputSeqEdit::drawRecordingSymbol(int startX, QPainter& p, const QStyleOptio // ------------------------------------------------------------------------------------------------- int InputSeqEdit::drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text) { - const auto r = QRect(QPoint(startX + option.rect.left(), option.rect.top()), - option.rect.bottomRight()); - - p.save(); - p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); - QRect br; - p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); - p.restore(); - - return br.width(); + return ::drawPlaceHolderText(startX, p, option, text, true); } // ------------------------------------------------------------------------------------------------- @@ -384,8 +398,23 @@ void InputSeqDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti // Our custom drawing of the KeyEventSequence... const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; - drawKeyEventSequence(xPos, *painter, option, imModel->configData(index).deviceSequence); + const auto keySeq = imModel->configData(index).deviceSequence; + const auto selHoldButton = [keySeq](){ + using namespace ReservedKeyEventSequence; + for (auto& button : HoldButtonsInfo) { + if (keySeq == button.keqEventSeq) return button; + } + return ReservedKeyEventSeqInfo(); + }(); + if (!selHoldButton.keqEventSeq.empty()) + { + drawPlaceHolderText(xPos, *painter, option, selHoldButton.name, false); + } + else + { + drawKeyEventSequence(xPos, *painter, option, imModel->configData(index).deviceSequence); + } if (option.state & QStyle::State_HasFocus) { drawCurrentIndicator(*painter, option); } @@ -479,3 +508,27 @@ QSize InputSeqDelegate::sizeHint(const QStyleOptionViewItem& option, } return QStyledItemDelegate::sizeHint(option, index); } + +#include +// ------------------------------------------------------------------------------------------------- +void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* model, + const QModelIndex& index, const QPoint& globalPos) +{ + if (!index.isValid() || !model) return; + + auto reservedInputs = model->inputMapper()->getReservedInputs(); + if (!reservedInputs.empty()) + { + QMenu* menu = new QMenu(parent); + + for (const auto& button : reservedInputs) { + const auto qaction = menu->addAction(button.name); + connect(qaction, &QAction::triggered, this, [model, index, button](){ + model->setInputSequence(index, button.keqEventSeq); + }); + } + + menu->exec(globalPos); + menu->deleteLater(); + } +} diff --git a/src/inputseqedit.h b/src/inputseqedit.h index 3a7080b8..f0766dd7 100644 --- a/src/inputseqedit.h +++ b/src/inputseqedit.h @@ -8,6 +8,7 @@ // ------------------------------------------------------------------------------------------------- class QStyleOptionFrame; +class InputMapConfigModel; // ------------------------------------------------------------------------------------------------- class InputSeqEdit : public QWidget @@ -68,6 +69,8 @@ class InputSeqDelegate : public QStyledItemDelegate QWidget *createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const override; void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel*, const QModelIndex&) const override; + void inputSeqContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, + const QPoint& globalPos); static void drawCurrentIndicator(QPainter &p, const QStyleOption& option); diff --git a/src/spotlight.cc b/src/spotlight.cc index fa04e6ef..f33e2771 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -1,7 +1,6 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "spotlight.h" -#include "deviceinput.h" #include "hidpp.h" #include "logging.h" #include "settings.h" @@ -10,7 +9,7 @@ #include #include #include -#include +#include #include #include @@ -19,6 +18,7 @@ DECLARE_LOGGING_CATEGORY(device) DECLARE_LOGGING_CATEGORY(hid) +DECLARE_LOGGING_CATEGORY(input) namespace { const auto hexId = logging::hexId; @@ -180,6 +180,32 @@ int Spotlight::connectDevices() connect(im, &InputMapper::actionMapped, this, [this](std::shared_ptr action) { + if (!(action->isRepeated()) && m_holdButtonStatus.numEvents() > 0) return; + + static auto sign = [](int i) { return i/abs(i); }; + auto emitNativeKeySequence = [this](const NativeKeySequence& ks) + { + if (!m_virtualDevice) return; + + std::vector events; + events.reserve(5); // up to 3 modifier keys + 1 key + 1 syn event + for (const auto& ke : ks.nativeSequence()) + { + for (const auto& ie : ke) + events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); + + m_virtualDevice->emitEvents(events); + events.resize(0); + }; + }; + + if (action->type() == Action::Type::KeySequence) + { + const auto keySequenceAction = static_cast(action.get()); + logInfo(input) << "Emitting Key Sequence:" << keySequenceAction->keySequence.toString(); + emitNativeKeySequence(keySequenceAction->keySequence); + } + if (action->type() == Action::Type::CyclePresets) { auto it = std::find(m_settings->presets().cbegin(), m_settings->presets().cend(), lastPreset); @@ -193,10 +219,35 @@ int Spotlight::connectDevices() m_settings->loadPreset(lastPreset); } } - else if (action->type() == Action::Type::ToggleSpotlight) + + if (action->type() == Action::Type::ToggleSpotlight) { m_settings->setOverlayDisabled(!m_settings->overlayDisabled()); } + + if (action->type() == Action::Type::ScrollHorizontal || action->type() == Action::Type::ScrollVertical) + { + if (!m_virtualDevice) return; + + int param = 0; + uint16_t wheelType = (action->type() == Action::Type::ScrollHorizontal) ? REL_HWHEEL : REL_WHEEL; + if (action->type() == Action::Type::ScrollHorizontal) param = static_cast(action.get())->param; + if (action->type() == Action::Type::ScrollVertical) param = static_cast(action.get())->param; + + if (param) + for (int j=0; jemitEvents({{{},EV_REL, wheelType, sign(param)}}); + } + + if (action->type() == Action::Type::VolumeControl) + { + auto param = static_cast(action.get())->param; + if (param) QProcess::execute("amixer", + QStringList({"set", "Master", + tr("%1\%%2").arg(abs(param)).arg(sign(param)==1?"+":"-"), + "-q"})); + + } }); connect(m_settings, &Settings::presetLoaded, this, [](const QString& preset){ @@ -388,11 +439,11 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) switch (buttonCode) { case 0xda: logDebug(hid) << "Next Hold Event "; - m_holdButtonStatus.setButton(ActiveHoldButton::Next); + m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Next); break; case 0xdc: logDebug(hid) << "Back Hold Event "; - m_holdButtonStatus.setButton(ActiveHoldButton::Back); + m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Back); break; case 0x00: // hold event over. @@ -408,20 +459,47 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) auto byteToRel = [](int i){return ( (i<128) ? i : 256-i);}; // convert the byte to relative motion in x or y int x = byteToRel(readVal.at(5)); int y = byteToRel(readVal.at(7)); - //logInfo(device) << byteToRel(readVal.at(4)) <<", "<< x<<", "<< byteToRel(readVal.at(6)) <<", "<configuration(); + auto holdBtnKeys = m_holdButtonStatus.keyEventSeq(); + if (holdBtnKeys.empty()) return MappedAction(); + for (auto c: conf) + { + if (c.first == holdBtnKeys) return c.second; + } + return MappedAction(); + }(); + + if (mappedAction.action && !mappedAction.action->empty()) + { + auto action = mappedAction.action; + + if (action->type() == Action::Type::ScrollHorizontal) + { + const auto scrollHAction = static_cast(action.get()); + scrollHAction->param = -(abs(x) > 60? 60 : x)/20; // reduce the values from Spotlight device + } + if (action->type() == Action::Type::ScrollVertical) + { + const auto scrollVAction = static_cast(action.get()); + scrollVAction->param = (abs(y) > 60? 60 : y)/20; // reduce the values from Spotlight device + } + if(action->type() == Action::Type::VolumeControl) + { + const auto volumeControlAction = static_cast(action.get()); + volumeControlAction->param = -y/20; // reduce the values from Spotlight device + } + + // feed the keystroke to InputMapper + for (auto key_event: m_holdButtonStatus.keyEventSeq()) + { + // key_event do not have SYN event at end. Add SYN event before sending to inputMapper. + if (key_event.back().type != EV_SYN) key_event.emplace_back(EV_SYN, SYN_REPORT, 0); + connection.inputMapper()->addEvents(key_event); + } + } m_holdButtonStatus.addEvent(); } } diff --git a/src/spotlight.h b/src/spotlight.h index 51c65861..e3b56677 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -8,28 +8,39 @@ #include #include "devicescan.h" +#include "deviceinput.h" class QTimer; class Settings; class VirtualDevice; -// ----------------------------------------------------------------------------------------------- -enum class ActiveHoldButton : uint8_t { None, Next, Back }; - // ----------------------------------------------------------------------------------------------- struct HoldButtonStatus { - void setButton(ActiveHoldButton b){ _button = b; _numEvents=0; }; + enum class HoldButtonType : uint8_t { None, Next, Back }; + + void setButton(HoldButtonType b){ _button = b; _numEvents=0; }; auto getButton() const { return _button; } int numEvents() const { return _numEvents; }; void addEvent(){ _numEvents++; }; - void reset(){ setButton(ActiveHoldButton::None); }; + void reset(){ setButton(HoldButtonType::None); }; + auto keyEventSeq() { + using namespace ReservedKeyEventSequence; + switch (_button){ + case HoldButtonType::Next: + return NextHoldInfo.keqEventSeq; + case HoldButtonType::Back: + return BackHoldInfo.keqEventSeq; + case HoldButtonType::None: + return KeyEventSequence(); + } + return KeyEventSequence(); + }; private: - ActiveHoldButton _button = ActiveHoldButton::None; + HoldButtonType _button = HoldButtonType::None; unsigned long _numEvents = 0; }; - /// Class handling spotlight device connections and indicating if a device is sending /// sending mouse move events. class Spotlight : public QObject From 17aa84fc6d99e346059b0cc26f43af3e0913618b Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sun, 8 Aug 2021 17:58:12 +0530 Subject: [PATCH 040/110] Refactoring of different subDeviceConnection classes Renamed setHIDProtocol function to setHIDppProtocol, as this function set HID++ version not HID version. Additionally, moved the function to SubHidppConnection class. Moved the signals in SubHidrawConnection class to SubHidppConnection class, as HIDraw device is not supposed to return any message. Hence, there is no possiblity of these signals getting fired up on data packet response from HIDraw device. Therefore, these signals should not belong to SubHidrawConnection class. Refactored the addEvents function in deviceinput.cc. Additionally, now the user is informed about the device getting online or offline properly. --- src/device.cc | 38 +++++++++-------- src/device.h | 37 ++++++++--------- src/deviceinput.cc | 98 +++++++++++++++++++++----------------------- src/deviceinput.h | 4 +- src/deviceswidget.cc | 1 + src/spotlight.cc | 64 +++++++++++++++-------------- src/spotlight.h | 2 + src/virtualdevice.cc | 9 ++-- 8 files changed, 128 insertions(+), 125 deletions(-) diff --git a/src/device.cc b/src/device.cc index d9c9cacf..2fb630bc 100644 --- a/src/device.cc +++ b/src/device.cc @@ -142,8 +142,7 @@ void DeviceConnection::queryBatteryStatus() { if (sd.second->type() == ConnectionType::Hidraw && sd.second->mode() == ConnectionMode::ReadWrite) { - const auto hidrawConn = std::static_pointer_cast(sd.second); - hidrawConn->queryBatteryStatus(); + if (sd.second->hasFlags(DeviceFlag::ReportBattery)) sd.second->queryBatteryStatus(); } } } @@ -172,7 +171,7 @@ void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, const DeviceId& id, ConnectionType type, ConnectionMode mode) - : type(type), mode(mode), busType(id.busType), phys(sd.phys), devicePath(sd.deviceFile) + : type(type), mode(mode), parentDeviceID(id), devicePath(sd.deviceFile) {} // ------------------------------------------------------------------------------------------------- @@ -362,15 +361,14 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca connection->createSocketNotifiers(devfd); connection->m_inputMapper = dc.inputMapper(); - connection->m_details.phys = sd.phys; return connection; } // ------------------------------------------------------------------------------------------------- -void SubHidrawConnection::queryBatteryStatus() {} +void SubDeviceConnection::queryBatteryStatus() {} // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::pingSubDevice() +void SubHidppConnection::pingSubDevice() { constexpr uint8_t rootID = 0x00; // root ID is always 0x00 in any logitech device const uint8_t pingCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rootID, 0x1d, 0x00, 0x00, 0x5d}; @@ -378,12 +376,21 @@ void SubDeviceConnection::pingSubDevice() } // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::setHIDppProtocol(float version) { +void SubHidppConnection::setHIDppProtocol(float version) { // Inform user about the online status of device. if (version > 0) { - if (m_details.HIDppProtocolVer < 0) logInfo(hid) << "HID Device with path" << path() << tr("is now active with protocol version %1.").arg(version); + if (m_details.HIDppProtocolVer < 0) + { + logDebug(hid) << "HID++ Device with path" << path() << "is now active."; + logDebug(hid) << "HID++ protocol version" << tr("%1.").arg(version); + emit activated(); + } } else { - if (m_details.HIDppProtocolVer > 0) logInfo(hid) << "HID Device with path" << path() << "got deactivated."; + if (m_details.HIDppProtocolVer > 0) + { + logDebug(hid) << "HID++ Device with path" << path() << "got deactivated."; + emit deactivated(); + } } m_details.HIDppProtocolVer = version; } @@ -397,7 +404,7 @@ void SubHidppConnection::initialize() constexpr int delay_ms = 20; int msgCount = 0; // Reset device: get rid of any device configuration by other programs ------- - if (m_details.busType == BusType::Usb) + if (m_details.parentDeviceID.busType == BusType::Usb) { QTimer::singleShot(delay_ms*msgCount, this, [this](){ const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_GET_FEATURE, 0x00, 0x00, 0x00, 0x00}; @@ -448,8 +455,8 @@ void SubHidppConnection::initialize() logDebug(hid) << "SubDevice" << path() << "can send next and back hold event."; } } else { - logWarn(hid) << "Loading FeatureSet for" << path() << "failed. Device might be inactive."; - logInfo(hid) << "Press any button on device to activate it."; + logWarn(hid) << "Loading FeatureSet for" << path() << "failed."; + logInfo(hid) << "Device might be inactive. Press any button on device to activate it."; } setFlags(featureFlags, true); setReadNotifierEnabled(true); @@ -466,18 +473,17 @@ void SubHidppConnection::initialize() } // Device Resetting complete ------------------------------------------------- - if (m_details.busType == BusType::Usb) { + if (m_details.parentDeviceID.busType == BusType::Usb) { // Ping spotlight device for checking if is online // the response will have the version for HID++ protocol. QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); msgCount++; - } else if (m_details.busType == BusType::Bluetooth) { + } else if (m_details.parentDeviceID.busType == BusType::Bluetooth) { // Bluetooth connection do not respond to ping. // Hence, we are faking a ping response here. // Bluetooth connection mean HID++ v2.0+. // Setting version to 6.4: same as USB connection. setHIDppProtocol(6.4); - emit receivedPingResponse(); } // Enable Next and back button on hold functionality. @@ -575,7 +581,7 @@ SubHidppConnection::~SubHidppConnection() = default; // For converting standard short length data to long length data, change the first byte to 0x11 and // pad the end of message with 0x00 to acheive the length of 20. - if (m_details.busType == BusType::Bluetooth) + if (m_details.parentDeviceID.busType == BusType::Bluetooth) { if (HIDPP::isMessageForUsb(msg)) { diff --git a/src/device.h b/src/device.h index a928425f..fafdade0 100644 --- a/src/device.h +++ b/src/device.h @@ -141,10 +141,9 @@ struct SubDeviceConnectionDetails { ConnectionType type; ConnectionMode mode; - BusType busType = BusType::Unknown; + DeviceId parentDeviceID; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; - QString phys; QString devicePath; float HIDppProtocolVer = -1; // set after ping to HID sub-device; If positive then Hidraw device is online. }; @@ -178,21 +177,10 @@ class SubDeviceConnection : public QObject, public async::Async 0); }; - void setHIDppProtocol(float version); - float getHIDppProtocol() const { return m_details.HIDppProtocolVer; }; - - // Base implementation of generic write methods to the device does nothing. - virtual ssize_t sendData(const QByteArray& msg); - virtual ssize_t sendData(const void* msg, size_t msgLen); - auto type() const { return m_details.type; }; auto mode() const { return m_details.mode; }; auto isGrabbed() const { return m_details.grabbed; }; auto flags() const { return m_details.deviceFlags; }; - const auto& phys() const { return m_details.phys; }; const auto& path() const { return m_details.devicePath; }; inline bool hasFlags(DeviceFlags f) const { return ((flags() & f) == f); } @@ -201,7 +189,15 @@ class SubDeviceConnection : public QObject, public async::Async 0); }; void initialize(); const HIDPP::FeatureSet* getFeatureSet(); +signals: + void receivedBatteryInfo(QByteArray batteryData); + void activated(); + void deactivated(); + private: std::unique_ptr m_featureSet; }; diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 90201c8a..60f89dda 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -525,23 +525,16 @@ void InputMapper::Impl::sequenceTimeout() if (m_lastState.first == DeviceKeyMap::Result::Valid) { // Last input event was part of a valid key sequence, but timeout hit // So we emit our stored event so far to the virtual device - if (m_vdev && m_events.size()) - { - m_vdev->emitEvents(m_events); - } + if (m_vdev) m_vdev->emitEvents(m_events); resetState(); } else if (m_lastState.first == DeviceKeyMap::Result::PartialHit) { // Last input could have triggered an action, but we needed to wait for the timeout, since // other sequences could have been possible. - if (m_lastState.second) - { - execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); - } - else if (m_vdev && m_events.size()) + if (m_vdev) { - m_vdev->emitEvents(m_events); - m_events.resize(0); + if (m_lastState.second) execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); + else m_vdev->emitEvents(m_events); } resetState(); } @@ -628,13 +621,12 @@ void InputMapper::setKeyEventInterval(int interval) // ------------------------------------------------------------------------------------------------- void InputMapper::addEvents(const input_event* input_events, size_t num) { - if (num == 0 || (!impl->m_vdev)) return; + if (!(num && impl->m_vdev)) return; // If no key mapping is configured ... if (!impl->m_recordingMode && !impl->m_keymap.hasConfig()) { - if (impl->m_vdev) { // ... forward events to virtual device if it exists... - impl->m_vdev->emitEvents(input_events, num); - } // ... end return + // ... forward events to virtual device if it exists... + if (impl->m_vdev) impl->m_vdev->emitEvents(input_events, num); return; } @@ -666,57 +658,35 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) const auto res = impl->m_keymap.feed(input_events, num-1); // exclude syn event for keymap feed + // Add current events to the buffered events + impl->m_events.reserve(impl->m_events.size() + num); + std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); + if (res == DeviceKeyMap::Result::Miss) - { // key sequence miss, send all buffered events so far + current event + { // key sequence miss, send all buffered events so far impl->m_seqTimer->stop(); - if (impl->m_vdev) - { - if (impl->m_events.size()) { - impl->m_vdev->emitEvents(impl->m_events); - impl->m_events.resize(0); - } - impl->m_vdev->emitEvents(input_events, num); - } - impl->m_keymap.resetState(); - } - else if (res == DeviceKeyMap::Result::Valid) - { // KeyEvent is part of valid key sequence. - impl->m_lastState = std::make_pair(res, impl->m_keymap.state()); - impl->m_seqTimer->start(); - if (impl->m_vdev) { - impl->m_events.reserve(impl->m_events.size() + num); - std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); - } + impl->m_vdev->emitEvents(impl->m_events); + + impl->resetState(); } else if (res == DeviceKeyMap::Result::Hit) { // Found a valid key sequence impl->m_seqTimer->stop(); - if (impl->m_vdev) - { - if (const auto pos = impl->m_keymap.state()) { - impl->execAction(pos->action, DeviceKeyMap::Result::Hit); - } - else - { - if (impl->m_events.size()) impl->m_vdev->emitEvents(impl->m_events); - impl->m_vdev->emitEvents(input_events, num); - } - } + if (const auto pos = impl->m_keymap.state()) impl->execAction(pos->action, res); + else impl->m_vdev->emitEvents(impl->m_events); + impl->resetState(); } - else if (res == DeviceKeyMap::Result::PartialHit) - { // Found a valid key sequence, but are still more valid sequences possible -> start timer + else if (res == DeviceKeyMap::Result::Valid || res == DeviceKeyMap::Result::PartialHit) + { // KeyEvent is either a part of valid key sequence or Partial Hit. + // In both case, save the current state and start timer impl->m_lastState = std::make_pair(res, impl->m_keymap.state()); impl->m_seqTimer->start(); - if (impl->m_vdev) { - impl->m_events.reserve(impl->m_events.size() + num); - std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); - } } } // ------------------------------------------------------------------------------------------------- -void InputMapper::addEvents(const KeyEvent key_event) +void InputMapper::addEvents(KeyEvent key_event) { if (key_event.empty()) addEvents({}, 0); @@ -725,6 +695,9 @@ void InputMapper::addEvents(const KeyEvent key_event) return ie; }; + // If key_event do not have SYN event at end, add SYN event before sending to inputMapper. + if (key_event.back().type != EV_SYN) key_event.emplace_back(EV_SYN, SYN_REPORT, 0); + std::vector events; for (size_t i=0; i < key_event.size(); i++) { @@ -766,3 +739,24 @@ const InputMapConfig& InputMapper::configuration() const { return impl->m_config; } + +// ------------------------------------------------------------------------------------------------- +std::shared_ptr InputMapper::getAction(KeyEventSequence kes) +{ + if (kes.empty()) return nullptr; + + KeyEventSequence kesWithoutSYNEvent; // InputMapper save KeyEventSequence without ending EV_SYN event + for (auto ke: kes) + { + if (ke.back().type == EV_SYN) ke.pop_back(); + kesWithoutSYNEvent.emplace_back(ke); + } + + auto conf = configuration(); + const auto find_it = std::find_if(conf.cbegin(), conf.cend(), [kesWithoutSYNEvent](auto c) { + return kesWithoutSYNEvent == c.first; + }); + + if (find_it != conf.cend() && find_it->second.action) return find_it->second.action; + return nullptr; +} diff --git a/src/deviceinput.h b/src/deviceinput.h index 553c6575..911e6910 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -306,7 +306,7 @@ class InputMapper : public QObject // input_events = complete sequence including SYN event void addEvents(const struct input_event input_events[], size_t num); - void addEvents(const KeyEvent key_events); + void addEvents(KeyEvent key_events); bool recordingMode() const; void setRecordingMode(bool recording); @@ -323,6 +323,8 @@ class InputMapper : public QObject void setConfiguration(InputMapConfig&& config); const InputMapConfig& configuration() const; + std::shared_ptr getAction(KeyEventSequence kes); + signals: void configurationChanged(); void recordingModeChanged(bool recording); diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 302e8839..0619a27e 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -253,6 +253,7 @@ QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) connect(this, &DevicesWidget::currentDeviceChanged, this, [this, spotlight](){updateDeviceDetails(spotlight);}); connect(spotlight, &Spotlight::deviceActivated, this, [this, spotlight](const DeviceId& d){if (d==currentDeviceId()) updateDeviceDetails(spotlight);}); + connect(spotlight, &Spotlight::deviceDeactivated, this, [this, spotlight](){updateDeviceDetails(spotlight);}); layout->addWidget(m_deviceDetailsTextEdit); return diWidget; diff --git a/src/spotlight.cc b/src/spotlight.cc index f33e2771..d5635756 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -147,11 +147,28 @@ int Spotlight::connectDevices() auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc); if (addHidppInputHandler(hidppCon)) { - // connect to hidraw sub connection signals - connect(&*hidppCon, &SubHidrawConnection::receivedBatteryInfo, + // connect to hidpp sub connection signals + connect(&*hidppCon, &SubHidppConnection::receivedBatteryInfo, dc.get(), &DeviceConnection::setBatteryInfo); - connect(&*hidppCon, &SubHidrawConnection::receivedPingResponse, dc.get(), - [this, dc]() { emit deviceActivated(dc->deviceId(), dc->deviceName()); }); + auto hidppActivated = [this, dc]() { + if (std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), + dc->deviceId()) == m_activeDeviceIds.cend()) { + logInfo(device) << dc->deviceName() << "is now active."; + m_activeDeviceIds.emplace_back(dc->deviceId()); + emit deviceActivated(dc->deviceId(), dc->deviceName()); + } + }; + auto hidppDeactivated = [this, dc]() { + auto it = std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), dc->deviceId()); + if (it != m_activeDeviceIds.cend()) { + logInfo(device) << dc->deviceName() << "is deactivated."; + m_activeDeviceIds.erase(it); + emit deviceDeactivated(dc->deviceId(), dc->deviceName()); + } + }; + connect(&*hidppCon, &SubHidppConnection::activated, dc.get(), hidppActivated); + connect(&*hidppCon, &SubHidppConnection::deactivated, dc.get(), hidppDeactivated); + connect(&*hidppCon, &SubHidppConnection::destroyed, dc.get(), hidppDeactivated); return hidppCon; } @@ -396,8 +413,10 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) if (readVal.at(0) == HIDPP::Bytes::SHORT_MSG) // Logitech HIDPP SHORT message: 7 byte long { - if (readVal.at(2) == HIDPP::Bytes::SHORT_WIRELESS_NOTIFICATION_CODE) { // wireless notification from USB dongle - auto connection_status = readVal.at(4) & (1<<6); // should be zero for successful connection + // wireless notification from USB dongle + if (readVal.at(2) == HIDPP::Bytes::SHORT_WIRELESS_NOTIFICATION_CODE) { + auto connection_status = readVal.at(4) & (1<<6); // should be zero for working connection between + // USB dongle and Spotlight device. if (connection_status) { // connection between USB dongle and spotlight device broke connection.setHIDppProtocol(-1); } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. @@ -408,15 +427,16 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) if (readVal.at(0) == HIDPP::Bytes::LONG_MSG) // Logitech HIDPP LONG message: 20 byte long { + // response to ping auto rootID = connection.getFeatureSet()->getFeatureID(FeatureCode::Root); if (readVal.at(2) == rootID) { - if (readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) { // response to ping + if (readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) { auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; connection.setHIDppProtocol(protocolVer); - if (connection.isOnline()) emit connection.receivedPingResponse(); } } + // Wireless Notification from the Spotlight device auto wirelessNotificationID = connection.getFeatureSet()->getFeatureID(FeatureCode::WirelessDeviceStatus); if (wirelessNotificationID && readVal.at(2) == wirelessNotificationID) { // Logitech spotlight presenter unit got online. if (!connection.isOnline()) connection.initialize(); @@ -435,7 +455,7 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) { auto eventCode = static_cast(readVal.at(3)); auto buttonCode = static_cast(readVal.at(5)); - if (eventCode == 0x00) { // hold start events + if (eventCode == 0x00) { // hold start/stop events switch (buttonCode) { case 0xda: logDebug(hid) << "Next Hold Event "; @@ -459,23 +479,10 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) auto byteToRel = [](int i){return ( (i<128) ? i : 256-i);}; // convert the byte to relative motion in x or y int x = byteToRel(readVal.at(5)); int y = byteToRel(readVal.at(7)); - auto im = connection.inputMapper(); + auto action = connection.inputMapper()->getAction(m_holdButtonStatus.keyEventSeq()); - auto mappedAction = [im, this](){ - auto conf = im->configuration(); - auto holdBtnKeys = m_holdButtonStatus.keyEventSeq(); - if (holdBtnKeys.empty()) return MappedAction(); - for (auto c: conf) - { - if (c.first == holdBtnKeys) return c.second; - } - return MappedAction(); - }(); - - if (mappedAction.action && !mappedAction.action->empty()) + if (action && !action->empty()) { - auto action = mappedAction.action; - if (action->type() == Action::Type::ScrollHorizontal) { const auto scrollHAction = static_cast(action.get()); @@ -492,13 +499,8 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) volumeControlAction->param = -y/20; // reduce the values from Spotlight device } - // feed the keystroke to InputMapper - for (auto key_event: m_holdButtonStatus.keyEventSeq()) - { - // key_event do not have SYN event at end. Add SYN event before sending to inputMapper. - if (key_event.back().type != EV_SYN) key_event.emplace_back(EV_SYN, SYN_REPORT, 0); - connection.inputMapper()->addEvents(key_event); - } + // feed the keystroke to InputMapper and let it trigger the associated action + for (auto key_event: m_holdButtonStatus.keyEventSeq()) connection.inputMapper()->addEvents(key_event); } m_holdButtonStatus.addEvent(); } diff --git a/src/spotlight.h b/src/spotlight.h index e3b56677..923f8a4f 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -73,6 +73,7 @@ class Spotlight : public QObject void deviceConnected(const DeviceId& id, const QString& name); void deviceDisconnected(const DeviceId& id, const QString& name); void deviceActivated(const DeviceId& id, const QString& name); + void deviceDeactivated(const DeviceId& id, const QString& name); void subDeviceConnected(const DeviceId& id, const QString& name, const QString& path); void subDeviceDisconnected(const DeviceId& id, const QString& name, const QString& path); void anySpotlightDeviceConnectedChanged(bool connected); @@ -93,6 +94,7 @@ class Spotlight : public QObject const Options m_options; std::map> m_deviceConnections; + std::vector m_activeDeviceIds; QTimer* m_activeTimer = nullptr; QTimer* m_connectionTimer = nullptr; diff --git a/src/virtualdevice.cc b/src/virtualdevice.cc index 2ca2ad96..658ea8d4 100644 --- a/src/virtualdevice.cc +++ b/src/virtualdevice.cc @@ -95,6 +95,8 @@ std::shared_ptr VirtualDevice::create(const char* name, void VirtualDevice::emitEvents(const struct input_event input_events[], size_t num) { + if (!num) return; + if (const ssize_t sz = sizeof(input_event) * num) { const auto bytesWritten = write(m_uinpFd, input_events, sz); if (bytesWritten != sz) { @@ -105,11 +107,6 @@ void VirtualDevice::emitEvents(const struct input_event input_events[], size_t n void VirtualDevice::emitEvents(const std::vector& events) { - if (const ssize_t sz = sizeof(input_event) * events.size()) { - const auto bytesWritten = write(m_uinpFd, events.data(), sz); - if (bytesWritten != sz) { - logError(virtualdevice) << VirtualDevice_::tr("Error while writing to virtual device."); - } - } + emitEvents(events.data(), events.size()); } From 68d4dbae2d5ced15e8919feb0dee4672f243703f Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sun, 8 Aug 2021 18:26:59 +0530 Subject: [PATCH 041/110] Added support for resetting pointer speed during initialization Logitech Spotlight can be configured for different pointer speed. A total of 10 levels (0x00 to 0x09) are possible. For more information, please check the setPointerSpeed function in SubHidppConnection class. Additionally, during initialization the pointer speed is reset to 5th level (0x04). TODO: Provide option is Preferences dialog box to change the pointer speed. --- src/device.cc | 15 +++++++++++++++ src/device.h | 2 ++ src/deviceswidget.cc | 5 +++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/device.cc b/src/device.cc index 2fb630bc..17e5904d 100644 --- a/src/device.cc +++ b/src/device.cc @@ -454,6 +454,7 @@ void SubHidppConnection::initialize() reservedInputs.emplace_back(ReservedKeyEventSequence::BackHoldInfo); logDebug(hid) << "SubDevice" << path() << "can send next and back hold event."; } + if (m_featureSet->supportFeatureCode(FeatureCode::PointerSpeed)) featureFlags |= DeviceFlags::PointerSpeed; } else { logWarn(hid) << "Loading FeatureSet for" << path() << "failed."; logInfo(hid) << "Device might be inactive. Press any button on device to activate it."; @@ -503,6 +504,9 @@ void SubHidppConnection::initialize() msgCount++; } } + + // Reset pointer speed to default level of 0x04 (5th level) + if (hasFlags(DeviceFlags::PointerSpeed)) setPointerSpeed(0x04); } // ------------------------------------------------------------------------------------------------- @@ -656,6 +660,17 @@ void SubHidppConnection::queryBatteryStatus() } } +void SubHidppConnection::setPointerSpeed(uint8_t level) +{ + const uint8_t psID = getFeatureSet()->getFeatureID(FeatureCode::PointerSpeed); + if (psID == 0x00) return; + + level = (level > 0x09) ? 0x09: level; // level should be in range of 0-9 + uint8_t pointerSpeed = 0x10 & level; // pointer speed sent to device are between 0x10 - 0x19 (hence ten speed levels) + const uint8_t pointerSpeedCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, psID, 0x1d, pointerSpeed, 0x00, 0x00}; + sendData(pointerSpeedCmd, sizeof(pointerSpeedCmd)); +} + // ------------------------------------------------------------------------------------------------- const HIDPP::FeatureSet* SubHidppConnection::getFeatureSet() { diff --git a/src/device.h b/src/device.h index fafdade0..8dfb7654 100644 --- a/src/device.h +++ b/src/device.h @@ -131,6 +131,7 @@ enum class DeviceFlag : uint32_t { ReportBattery = 1 << 17, ///< Device can report battery status NextHold = 1 << 18, ///< Device can be configured to send 'Next Hold' event. BackHold = 1 << 19, ///< Device can be configured to send 'Back Hold' event. + PointerSpeed = 1 << 20, ///< Device allows changing pointer speed. }; ENUM(DeviceFlag, DeviceFlags) @@ -272,6 +273,7 @@ class SubHidppConnection : public SubHidrawConnection void queryBatteryStatus() override; void sendVibrateCommand(uint8_t intensity, uint8_t length) override; void pingSubDevice(); + void setPointerSpeed(uint8_t level); void setHIDppProtocol(float version); float getHIDppProtocol() const { return m_details.HIDppProtocolVer; }; bool isOnline() const override { return (m_details.HIDppProtocolVer > 0); }; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 0619a27e..7bea1bca 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -160,6 +160,7 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) if (!!(f & DeviceFlag::ReportBattery)) flagList.push_back("Report_Battery"); if (!!(f & DeviceFlag::NextHold)) flagList.push_back("Next_Hold"); if (!!(f & DeviceFlag::BackHold)) flagList.push_back("Back_Hold"); + if (!!(f & DeviceFlag::PointerSpeed)) flagList.push_back("Pointer_Speed"); return flagList; }; for (const auto& sd: dc->subDevices()) { @@ -441,12 +442,12 @@ TimerTabWidget::TimerTabWidget(Settings* settings, QWidget* parent) }); connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::intensityChanged, this, - [settings, this](uint8_t intensity) { + [this](uint8_t intensity) { m_settings->setVibrationSettings(m_deviceId, m_vibrationSettingsWidget->length(), intensity); }); connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::lengthChanged, this, - [settings, this](uint8_t len) { + [this](uint8_t len) { m_settings->setVibrationSettings(m_deviceId, len, m_vibrationSettingsWidget->intensity()); }); From bf98b019a02a07ae8f53dc8d452b97bde12ca119 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sun, 8 Aug 2021 19:36:10 +0530 Subject: [PATCH 042/110] Improved Device Information in Device Details tab HID++ versions and features are shown separately with better formatting. --- src/device.h | 3 +- src/deviceswidget.cc | 68 +++++++++++++++++++++++++------------------- src/inputseqedit.cc | 2 +- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/device.h b/src/device.h index 8dfb7654..5c567c2a 100644 --- a/src/device.h +++ b/src/device.h @@ -199,6 +199,7 @@ class SubDeviceConnection : public QObject, public async::Async 0); }; void initialize(); diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 7bea1bca..e9cacd33 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -1,6 +1,7 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "deviceswidget.h" +#include "device.h" #include "device-vibration.h" #include "deviceinput.h" #include "iconwidgets.h" @@ -152,26 +153,16 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) if (m == ConnectionMode::ReadWrite) return "ReadWrite"; return "Unknown Access"; }; - // report special flags set by program (like vibration and others) - auto flagText = [](DeviceFlag f){ - QStringList flagList; - if (!!(f & DeviceFlag::Hidpp)) flagList.push_back("HID++"); - if (!!(f & DeviceFlag::Vibrate)) flagList.push_back("Vibration"); - if (!!(f & DeviceFlag::ReportBattery)) flagList.push_back("Report_Battery"); - if (!!(f & DeviceFlag::NextHold)) flagList.push_back("Next_Hold"); - if (!!(f & DeviceFlag::BackHold)) flagList.push_back("Back_Hold"); - if (!!(f & DeviceFlag::PointerSpeed)) flagList.push_back("Pointer_Speed"); - return flagList; - }; for (const auto& sd: dc->subDevices()) { if (sd.second->path().size()) { auto sds = sd.second; - auto flagInfo = flagText(sds->flags()); - subDeviceList.push_back(tr("%1\t[%2, %3, %4]").arg(sds->path(), - accessText(sds->mode()), - sds->isGrabbed()?"Grabbed":"", - flagInfo.isEmpty()?"":"Supports: " + flagInfo.join("; ") - )); + subDeviceList.push_back(tr("%1%2[%3, %4, %5]").arg( + sds->path(), + (sds->path().length()<18)?"\t\t":"\t", + accessText(sds->mode()), + sds->isGrabbed()?"Grabbed":"", + sds->hasFlags(DeviceFlags::Hidpp)?"HID++":"" + )); } } return subDeviceList; @@ -189,23 +180,34 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) }; auto sDevices = dc->subDevices(); - const bool isOnline = std::any_of(sDevices.cbegin(), sDevices.cend(), - [](const auto& sd){ - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - sd.second->isOnline());}); - const bool hasHIDPP = std::any_of(sDevices.cbegin(), sDevices.cend(), [](const auto& sd) - { + bool isOnline = false, hasBattery = false, hasHIDPP = false; + float HIDPPversion = -1; + QStringList HIDPPfeatureText; + auto HIDppSubDevice = std::find_if(sDevices.cbegin(), sDevices.cend(), [](const auto& sd){ return (sd.second->type() == ConnectionType::Hidraw && sd.second->mode() == ConnectionMode::ReadWrite && sd.second->hasFlags(DeviceFlag::Hidpp)); }); - const bool hasBattery = std::any_of(sDevices.cbegin(), sDevices.cend(), [](const auto& sd) + + if (HIDppSubDevice != sDevices.cend()) { - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - sd.second->hasFlags(DeviceFlag::ReportBattery)); - }); + auto dev = HIDppSubDevice->second; + hasHIDPP = true; + isOnline = dev->isOnline(); + hasBattery = dev->hasFlags(DeviceFlags::ReportBattery); + HIDPPversion = dev->getHIDppProtocol(); + // report HID++ features recognised by program (like vibration and others) + HIDPPfeatureText = [dev](){ + QStringList flagList; + if (dev->hasFlags(DeviceFlag::Vibrate)) flagList.push_back("Vibration"); + if (dev->hasFlags(DeviceFlag::ReportBattery)) flagList.push_back("Reports Battery"); + if (dev->hasFlags(DeviceFlag::NextHold)) flagList.push_back("Next Hold Button (Reprogrammed)"); + if (dev->hasFlags(DeviceFlag::BackHold)) flagList.push_back("Back Hold Button (Reprogrammed)"); + if (dev->hasFlags(DeviceFlag::PointerSpeed)) flagList.push_back("Variable Pointer Speed"); + return flagList; + }(); + } + auto batteryInfoText = [dc, batteryStatusText](){ auto batteryInfo= dc->getBatteryInfo(); // Only show battery percent while discharging. @@ -227,7 +229,13 @@ void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); if (hasBattery && isOnline) deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); - if (hasHIDPP && !isOnline) deviceDetails += tr("\n\t Device not active. Press any key on device to update.\n"); + if (hasHIDPP && !isOnline) deviceDetails += tr("\n\n\t Device not active. Press any key on device to update.\n"); + if (hasHIDPP && isOnline){ + deviceDetails += "\n"; + deviceDetails += tr("HID++ Version:\t%1\n").arg(HIDPPversion); + deviceDetails += tr("HID++ Features:\t%1\n").arg(HIDPPfeatureText.join(",\n\t\t")); + } + return deviceDetails; }; diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 45444ebf..81234ec1 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -88,7 +88,7 @@ namespace { { if (!drawEmptyPlaceholder) { return 0; } return InputSeqEdit::drawEmptyIndicator(startX, p, option); - } + } int sequenceWidth = 0; const int paddingX = static_cast(QStaticText(" ").size().width()); From 21580f3bb8876c4a0669e7cf9f42e25ec342b6ff Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sun, 8 Aug 2021 21:20:44 +0530 Subject: [PATCH 043/110] Added icons for new Actions --- icons/icon-font/.fontcustom-manifest.json | 26 ++++++--- icons/icon-font/svg/iconmonstr-audio-6.svg | 1 + .../svg/iconmonstr-cursor-21-rotated.svg | 54 ++++++++++++++++++ icons/icon-font/svg/iconmonstr-cursor-21.svg | 1 + icons/projecteur-icons.ttf | Bin 4368 -> 4688 bytes src/actiondelegate.cc | 12 ++-- src/projecteur-icons-def.h | 3 + 7 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 icons/icon-font/svg/iconmonstr-audio-6.svg create mode 100644 icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg create mode 100644 icons/icon-font/svg/iconmonstr-cursor-21.svg diff --git a/icons/icon-font/.fontcustom-manifest.json b/icons/icon-font/.fontcustom-manifest.json index 8a1ff0eb..9dfcd559 100644 --- a/icons/icon-font/.fontcustom-manifest.json +++ b/icons/icon-font/.fontcustom-manifest.json @@ -1,14 +1,14 @@ { "checksum": { - "previous": "a9504014a04ad54718a75911f452551882ad71440ff3fac45f0dad3a5042a9ef", - "current": "a9504014a04ad54718a75911f452551882ad71440ff3fac45f0dad3a5042a9ef" + "previous": "fdceee78baa623d59e66194333bacfa5e2256cd9a60fdbb02e780f8c59cfda1e", + "current": "fdceee78baa623d59e66194333bacfa5e2256cd9a60fdbb02e780f8c59cfda1e" }, "fonts": [ - "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.ttf", - "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.svg", - "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.woff", - "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.eot", - "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.woff2" + "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.ttf", + "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.svg", + "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.woff", + "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.eot", + "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.woff2" ], "glyphs": { "iconmonstr-arrow-73": { @@ -19,6 +19,10 @@ "codepoint": 61708, "source": "svg/iconmonstr-arrow-74.svg" }, + "iconmonstr-audio-6": { + "codepoint": 61724, + "source": "svg/iconmonstr-audio-6.svg" + }, "iconmonstr-battery-3": { "codepoint": 61696, "source": "svg/iconmonstr-battery-3.svg" @@ -47,6 +51,14 @@ "codepoint": 61701, "source": "svg/iconmonstr-control-panel-9.svg" }, + "iconmonstr-cursor-21": { + "codepoint": 61721, + "source": "svg/iconmonstr-cursor-21.svg" + }, + "iconmonstr-cursor-21-rotated": { + "codepoint": 61722, + "source": "svg/iconmonstr-cursor-21-rotated.svg" + }, "iconmonstr-gear-12": { "codepoint": 61702, "source": "svg/iconmonstr-gear-12.svg" diff --git a/icons/icon-font/svg/iconmonstr-audio-6.svg b/icons/icon-font/svg/iconmonstr-audio-6.svg new file mode 100644 index 00000000..1aed7b8c --- /dev/null +++ b/icons/icon-font/svg/iconmonstr-audio-6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg b/icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg new file mode 100644 index 00000000..658c78c5 --- /dev/null +++ b/icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/icon-font/svg/iconmonstr-cursor-21.svg b/icons/icon-font/svg/iconmonstr-cursor-21.svg new file mode 100644 index 00000000..5d62fc0c --- /dev/null +++ b/icons/icon-font/svg/iconmonstr-cursor-21.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/projecteur-icons.ttf b/icons/projecteur-icons.ttf index e37116c9de3fd3eee85f217c01616b0a76c524d3..90d45a70d111b8333bc7c0f584c6629b218e1265 100644 GIT binary patch delta 1215 zcmZuwUuaup6hG(tzI$)hv`O0JCQBREG=F-bT@sV_wgG2XVHSOm3GK2ytkaNNmo-gN zn@p!KExv3Fild(`3`FOP3f7keMMSX5)R%!nWlvQmdzueo@DD?<@!Yh|;daT%Ilpth z?{~lR-QT$sxe;3i1b{AB0S?bhP0c*Lv3BhaK+h;;Lr14mnWKNsrU38+;pYnF{NlCB z-F3ou$>s|$*2B<^M*z4*b#XdhTWqz633?6^cAQ>#`T1L!;n%2s1)w8boXcB%7cbwY z+BVr(u{gJ!=fC1tMCPMJtXQs}6QAQ8;g1PVE>sKo{#%hxh@eO~Th54k@>45I<2_J7aM-6`S*@aWjx)1#UPDa}RelRy1yPj<} z*6=aj2d~RX?8D#WNj!`p`34@JC6@L>?bt0rsLcjo5wb5W1OlEU?*a!fP$AsxYyTR3R?by2;M_396R5r+F*n8}EuJZl-6n~TdB@T*; zctu%fljF1VgXqw{SP8Os(=YrODvir73ulB^u1M3j?R&pY|N8TYE zHrxNmHKl_Enes3BbZ@Meejq>6K15S4I8tnMP`>3zGiyqI=P;C2OP_?R`~F6pkUJfZ zvWO}toN2ad$(l36tyQb>zViSsb69U}VF|ZD0}ljAFg`UhI69Uw%;CCT4y|L=a;V+?Tk9%q};?;9U61DY8f8`Yw^ zF|LoOzGdurfMES0uK^MRw0rqo7>qpI) z)OFOiHa74n|E#`qr^KMdX8k7-R#4x`&8Mfa*E5|?#m%^S^JZ>n=$IKd<=35~#xPvj z6Q-a``DHIwz=H#Ou}blbjC3W2awJ$1uh>V@4a4=lSfSXST02iN#tey9{@67jue-eR zgRT=w77~z^ovswmCbIId>uvdyD~x@TxgSx{j{)2-UvT>xQ|@kT74-| zIJ;DPx# delta 904 zcmZuvO-NKx6h7zP`+mk5-;6N+j7~D?)T?Y}oTrwQ*$8Dpn2Lr5W{x^J3r7c(uuVn_ zqfP!?T156ki-HzyhQO9WgtW=Ew1`|q5k4q45z~3oNShw-a?W?Y^K;KVXEVH1HxCE^ zrEm*4^mKOicgg+lR{?xK$QtbKi&@>fR}KT#Hv34GrO>l2eVt~K<2Z6tmPz}1>gCC3AA9^NJHBb{UEbZS1yckmr0^OGT* z&dkn0eCh5SVeYvX7<3Z zOWOd(C&HdWbJXC*i_4*_-nO6MW)w!%yCMsJC5tSq;3-}OPh z)Jow53z-0h2ssZH1OW+>9?*f29v}Z z8Tl$)6*?=%aztcIFj=OZ9sW-Mu}B$J#kCi)SFJ?og=(Cl@LxHVuB0kD3or$pyJ*7G zR~@dVU%u2{<8e8n$3#nW6y>rW=aKHHe5;#kXY69MJdxYce6lFS=(#hW5l#A>wSGHVd)o$%1~3txT<0& Date: Sun, 8 Aug 2021 21:27:14 +0530 Subject: [PATCH 044/110] Update Button mapping screenshot --- doc/screenshot-button-mapping.png | Bin 41283 -> 69589 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/screenshot-button-mapping.png b/doc/screenshot-button-mapping.png index ee832ebf59a382eaeea386495e8597fff1d76c3f..3d30008763f50dffeab1ac4f8f61445a39c6161d 100644 GIT binary patch literal 69589 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

f7@IAHSna%-Fwf1kEJsl z+ui`p)NoIf`=NuY#s|NQ{o=6DZ_dmLmbg;~?BkT{bw%|C!TGg|_4M@kyPPge70LjP zX~7$z3WvtrtJ!^5IMLCa)kcsN|ETWd80|p_&NC%SI!#}n(cy355ae7}L=?)tzEF>K zLCU6k+ij3p*VTsOHq#M$T{kT|VE|e>8{4>8f*`{kp=uE$WA6}UPe}0d5n0DGen?=uilPrswX+$jA9Ukou zB1O>|trwPCQ3WxXBwEx{5s;LC+?AobPmJfIq<&cDr0hY3XDQ!$90kv(vtfbdTCVN; z8)FidKi$ZD8VI#^+GNJ#oEZvcA1E ztb&F1XN&g8xwR;2nH8{It!yt8ew{#Ez@qtagwziAgbetACTd>nj28Hqsa&F%K(Ev4 zdqxTI`0_H_;)GeMT$1|leonF*=G#4DVO4Uk27Shols~$+@BH*w5bLIf!R*p!gz|22 z7i*(ry4$MHiC%$c!C^#7x7l?dQ~Y&7gN~`4GeZ=r>4yE}vs59l+ecH$dOUpE;M$T~ zSPZp7KKoba*li)y((~tL*TB9ioLNdOa&0j?;|C93fnp7#oAtljGCOoOO}mYkU_oy5>b7ZhlGQU72Apct&Wx>4*Oi*Mh11 zu3wV>dHRblW!8bmz**a^n zP{8WzCb%+N4?p9HThfBBl3M>Y@;b74_=4?))9-<*>gO%vG}3M)P)FA0IgT)xcH0$} zu!bfOnJQ=6gj!2$)hsbFk!zM4i#SjmP{^9znRP`E%gLX?&2PQy;~DY|?0x6QxdAtL zA8u|&6STRp!5*JCAsQK_4)sC&!^)r-H|*cQ>GNL~^brwDf3p;ojTWK+Nvlau0{8>m z`xm?bH=N)vJG}{v2tsv47QyCLeU#JoeGBHl{mAz$#KAvq-k{=fpHGbYnKAnv^7hUE z2*7azNx*O1^q_xy{KttQgo*zIXbibmtltV04<7#wLaumYzz+MrL4xKC`0oA*0E)@L zd-{J);%wGk9!=-fM+p5J8(bx*YiL-+%YxnVtSr5-fUo~{IYJZ@6N{V~X8rcxj{nQi z{(n39`56DlMqMK8c%&-Z%=mUl9?1HZ}-&d3kSdZ)dor za*X07|8Ck~uOP)UA8MoBLnx6}qchw{vEiK8=NSzEXuCy}01JFbex|(+83pC~N=yPA zR%)72IL8a-5V>8f@wi_SbqIa^4!&4dnh|s5`P1WLz8fWYQo_M;2N^-x+1bDYu~2bs zXJ=Mek+Hs}cu5m(G1AVWzioYxlq&yDZ0=r#Amf=%&Ivg+&NUm*+0^>Dajyoxt~lLw z=KpB|{|`zln61-w|7>PHT>r^|&mwvXo0i-qA7_Bzajp1%aQG47qH<)lDNU0X|5qh~} zrK)0|8YvA0X{wwmOib`pFi+d~>fY8i^t%rDc@qMEV1NbKsmINaAfcpGBuhq18?n~n zG~40zU_46$#lX@eM?=2OAhn1Xd0D)IJbR@7L6>iP#lK{1QaLsZQDcVHv|tj_^t@Rj zGnLm|x0g0$?pb|V?N5cFtmJ_-{rO@P9X^^%fkhnkm1oC%#xI6b@ z1@KLL(~MF<;W_`#*}33dhFmUF`p__g^--Ww>|zCVSsf4=5LTdL-6}9i%ijVKWvCI;fZ#EqPK$rPz8zdbj#TMcZ7wdn)n!3pbe3c&*YuqOYT%!skU9C|;e9zB% zc$MNvVLbIm99UL}wGX{@?s`-K9m^bcJU*JOiZdky_|S4@t1j zHCJ5+k3l?9S6j)3*n2eRQqdb-RCV_iq`xBhRYle3j6p7){QYXDKLy}s+dkPqr~*+< zEJnQbL~xJiCt=K7;MdS+#l@Ni0 z`!X=%Xd0`@a{QRdkz7m8T0g#nPH8fbVo4H9jsa-;_NTMHOe@He&XdXZjqd{5H`OOI zcF8%^A+xs`A_T~Du)EO(7$uaUp0OH)9&wK{9{D3154S1XG$Zr4I|k_fkfi!bIY6L_ z-yUvRA*#JR8mp9YyxD@$f;!52Mjj4OEw~P>!%DOoF6Jj^>dyNXa|pblx3wo-#)8;z zbaa$T=e8rSRu_5(v4>a)t!~Exl7E@cDl?R^7WLF1i?zAZ{v$);K4~FXBqRzFlAocd z6f=!BT8?`|kom^SkVGu>yoXcIn~6YQ{r9Y5I_^y2mEozcdr>!hQx>MN#DrXG{_3 zbQ8?Ut#8t&X-3gWjbb`M77Vk z+cgmR+4z6)%N?sB#*Cn=-N8D`dFo7FuYqJHBlT8ii*|(|CZhp!&Qj#Q`tMSv1m)s~ zd0xw{o?in1AjCoui@7?$dFouDql1?8?j6p^&1h=EE%nzVF{6`&+T55!22f`Oz10fl zcL6LlRnX8MC;BmdW;mp_r5&5Kj}ISwirG!!{T3^`_cYnn_h`flb_OAb6zX{rs5J_* zl?7YsL1Km4jz;%gPUZ;=_b9GLNGE8_e@cL&Y^Taw59p)E2Uu!%clNcnc_UNUXBV44 z`N+^q(6qQ=-N#8Gv}l;2gg3X3W@hCZ0IGHuPVeJI`KnQ5`Zr`3T|qxJ!e)xqXg=5L z4ZK-(P^k_e|D3Ebn$}7G%rVF{t;KQSU_MP?-jyLrjUVLB8rd@wMWP~OurBROQ;_em zKf}3VMxhj(hEeoX^SkzcSR^wChuKQK1q7{DV+0uND707JM)kSj;bc1-wzG zX~vLIscfMB{_{Xcu({7UmE-+2B0)0#O|InyLVX&lh(v@kx0dzo&Qn_%jLw`{y5y=I zjM6SU#UM$NK~gj_y<5mUvr-534D$RU40tEC)&A_LVU(h-ojULxlDmR$bV5SKy`dd* zLc}q4vVfPLZlnKhF$VjP7j6X)j>>WnybT3D;xJ2_L1x>mHa0gmLrm40j7C2&L}0Ug zMvY@|7ccA$EDxfdsU9^yu+bj8goQjyt4>#X8?G=5u=%>xW3}<&oxm!hr~(vwGQMLnO%Kw-B#Y@f26^=Iq}`HiE%KHRLdZppiA$_4;u@pDs?}& z?6{PPz<*}%FJAPZg+pB1j+``MN22fc<7e+_7}O*v6#VLg4xVAjT-0AbIp&#J`7byg zy5WRz$Cpf7)@4eOYzb?!zY$+IJIfnAq=dYX?so;_w| z!Ckv|q(ho>QROA*6Ml8ErYjQkah6#s33!%teiY(I2 z!Vh12I6z|XC2;NF>=<)vDg(tsx$AO!ZGVt;JSnMpSiA@GzWT}&aRrGXi0p@e0JS@J z_gaEAa9dVwu(@ba_-f};bs;1%yUZ#kI;hgjTpV%hY4zbp>&pj{Uy>*2EwEq{#|T`e zSz0OM<_$9o>)_Io5Wj5urYfy_T~km8C|YWuf%qgz60mtWcSvyfP^CXl!%=vqEk}qc zmGCFU0hc)>bP$)Rv<6yk@IFU(cugHSa5Er$ZR}+Dk^G)e2{+MFdegHHj8YJyJ;v-- z4xjrW;zVUgl)YvgpBY2|hZdBFj0FDR64DdG6|C6#IIzz%22aI-X7uR=5G>^@ztdZf z<+{5(K!mG(4`OnmN_nZjQY!%`t<6)*juhTHf~h){2AAw#0UE5Mr*9Mp#m!#@?RX-U_ zgmKX}nM(@<5pe00DR!gPu5v*Fmb*^%YLgoC0-Xfd0EPkyx^YJy#R&YKuG?4Hs>&lq zl2d(gcKPk7|gwSi2dbrYY5gVL04^+Tb&kU zTJJieRZdl?hNt2su=5sEGlAmDh@W|(PuxyFdZ{VabU~xDE}8XxIM;9;$mEtz66|=Ef&%i)1cyB85h6*M-Zh4}VEsAyk-Lg%%pYH`%@qLm^ z$ab$gKC{w{Q{+O??WLRSw#XFDTJPK01Pv)pd`il?pU!cxC|B`KJ25F6_(LaF((K9$ z9Yw$YP;?MquIyi#_At9fhIHlYW?DIxZ8AE{8Hva^L|go4d)s+MHai+O@Xi|z09}31mV&Y8{^nESRD%GoYSN~`l)HK zk*x|j3QfW&N5AovjyiU4qDSSpME{BfAI@UoU$Ka=KJS}ZS-qSsRA@=auF8%D!C_MB zw6ZEFSZ?&jM~FoeT)p`?I*rAd8W;$4dOvlV}J3Cv}iT4QI0M9u}O zp4V4^M*9vu&?&JX)`^1s%p$n1;=r!{EwytcyhnH-26FZ96|Q)HPyGCRwrbm5W#cIp z0~25!Z@UyZgTT^_P$T$F^q;%PiR+X!Bj)B#Jvz$Q#Ukp2_VhdkeIvV?esGnRT>U#I z#RG864o;6T|A|uzeAFFcqWT+<%6KPC{8TIfRaP3p-l4&IsCHI9ZbV5~c!7Hej|>g{ ze6%#C{&n#`&gFZU5fc&p`0D|_rORB_p$+4L5Q$0Fx}(h2nC8M8#(_B^NUrr(OM%4 zACS0VP)znfdqkAt`40+kBn4~ooB54S6pCFjN_R0i1II14nFvGOFsMiubjXK zmcv6o5#y%+fgXO@Wo2gSdWQU_{0p0z%asT}wAE@3Bop5sYmx6W1R}SJbHho5A|CX! z`1Q_iGbAgP%yp1a!|-o%c>j8P_Sn=~fFRBLTzM{kJ)YSGgnBQ=606$hj`^!rO?y%;!*290bJUicwPFO!m53yO-_f9S0l(h@V7%km!lj*EREBN zg2NxSR_oS2(7EvWHs8>;9xh7Nb}SR^Ql&gmHfl`C z(Z+ab>zeo1dOJPc1wBn&2~W59r?|w+vk+fnRceI~IfLZ-DrVFjjeJI`P*!X->n1r~ zRG&3zw-zQQmdXZ7E!O`uyhmH5#x_}#jiA#=?0+7&(Cs4eVD3lUO*QGE)F(B1yYDtq zaB?K)Xe~3%+7W)tFu7^f!Q=NxhV%bqU$rgz%6?v)xyibm3)@x~k68J-w7gBI`>KHn z=MowZY^n2OX_M94o+;VYMjR1Iah#YlKai-i3>uCV?gJ?|m0X4#JgIW8G#SOMFr-yq zzlmzD_Jj2PtU{-Qc?Pc(^bY4E9v41CVq9jJCti(t5(PZQGp?*UY0ruS^}vC+M&{;P zG6)U)8PgS0O?K%@QTnaLZ#AyaKxN~y+Dh}-!- zyS;)5BJ3v}r!(cje)-#A;nd0DQ9V}?Tnzj=p~fXBQbwz>+GN!9jiZt-;pW7_Y7_=1 z$+2KIzJmu|rYz#FhW9kW;vA*mI7e1E01|GEyQa|GR~{hVQH z%%|Ga*^--NZjRWHsqUbufCi1(k#Z|+QsFW@fOt;y*^0*I*?eV?w9smzDX@?NXIqxr^PmjLI1Fns=GxV#N5 zbH@j^wIh>QW*xiXkj&%s-srm$wC)+6>}zm`hWozGOd3MVK(4{-5_a<3+os{`kZCh} zMt`WAhJ!62LSg-M+x(JnmV$7*e=)rm2SX4-{w?Ia#$vYcqmP(}|vF7vK zQjt3=r>r9BB*W^Yvh7Wk+@;hFHEXhKJVex%(aM*1DstC(5o0zp4PmpR03EymN* zKn8x-#V09Z<>QMX!SvipRtuDO>Q+Z{zt%}yAs|!av6ZX84V0uo>N5TmojW7aUy*dr<80)z>Ee^IyqB~I!Ti=nH?wc0k{{p4fxXSI z7h`R$vGhSG_3{2jzrtqZZk|pg5|lt?eN69)Y73gvx$X6(eM6^bn(c(Awr%zePxa@` zSP7xJ+i%J81?Pp;wyG`vAVOT9 z{Xz<7d6cqWv@lg7eLXhTLrkjSLqXCZgu;Eww1~7Bu!c^l@?9rMk?{34)h`}WM0(D(fJ5)cAXON(fC?k=s802@P zS?NOW`P2QJ9v(<$Gs%mY@DHPBSAMLJ`kEgplB~OVK_I2yUBPz=F&3+Z&U8MOXK~ar zQE_uaH=_?M39IeBnVRR8;5?n-_0WN07Wt0{6FXhY8m^d5*$nQWq$IW2(GsPi8Ne7r zEHMBMg7SK*9Y|Ep^=AWaa>u6t{@!%6o$z>ZB|~xlbk<@BzESUi0`|uP9*#+LjsdDF zzw{JubnCFTn-Kh}?H2Z>wG#zA0vyY!WzpNqY{eRsW{&fTM~}AVJjY52 z?%+K#kDGAC0Nm_8Z>}0k<~}cX`IdAVI?u?x$qF@h)F+RXBO<%|@vQD2jck-^orpGm z$z#u?Wmx7mBMcOL>zq&4Qkc>*_$fYQ2k$`Il7N&KVF(Pi$;grts=_Eghy6MmIpgnX zIPqmmzv%nk_Oedjgvlc~K%Wj5tw-dsMqhhIiDS6gT5+gVG5=J&Vfh1`{Ji%3krbiORvqcUtxt~9#3#JRs9 z9lkZpeupYetX6%}&iX)R^|K2z2`EC|I{xSWxTpWzeKv4q-_g6V3Nay7F=+6tM!;wBAB0VV7|J+KkQX4_@{wX10 zkkCi<;?3-Es>Ihx6nIN48E$kYWRCpyBPdki`ViCQ>Qnk|cc)r1yScg*~w!e|~sFsasJl zxF1VXSG61+s^N90Ywp)Or%~|wg*ln4JktpnjX$V#AL|H_X&{IooW*V`Z%UcY#tauk z9k^oR2B*dv=Y36pn>rBYx1*-_z|G0GeuSYLht!K0 z*X%_5Oa1y(8tthj)I-*!p?V>9ANi{=aG*c5{qjdsaH&Vu&f=F*kIW)+d@<12R;xzy zqZf@Ol6HukyWxHec9Ub}pwE3layiMmeCz)g#7lOk63p zQw@_MBKz}UxSuNg(VhbnSh z(9(wE%ZJ|bGK660nrGdD=LJ#Kb{(6)62RuPVRpP%@nwV;B`a=p`4&ikanU0117lEK zU3_dl%4V0>o<=25aXxL(16!ONPg+ML%DbW0Hj-B>7Dso$F?gHrC6nF1%L zuRR<`%#T>8kXclIp#t|nP}W?e5+bn5USM@R-%wG{_tzgl8z^?2t{yM4_juhjxc?oA zs1jz7E7F!k=7VWvWfhD>7}e0wpt@P2<(zP_S@#?5)OruPJ+u7)|92$vTN-~lp--Fr z)*f1^M*lafsbcEo4bCZ(HF8bD6M-H_nd3wDC_+`dJ=H}Lv{`7FUC@Pyb^~ydR?S7u z2@>XTA#D0~StYd~Np-sB$Mm2bPusw&R3Fw%Hm6~rqs0}BXqKzl)AFbK5!}@Pi;bVD zLEK6PL-K`e#ecF~>OK&s>v6{FO)TnP`GuQX5w`Ja z@Ajx%Siwy(R)fOAURfk})vT*QnqJ3K(oXYh+I4uXfeE5V{KAbL@gk4{St3M8dQ?|MudCH|}%Ghv&?e{#^3k%pP^fPpm zBX5+m^LT|JVo90frR*R3Sq|ZA=YdpnbDHN~??MuAuV%84v#=+3eF02*-1y)=^2~G& z)99RIpd$A<{_bF^_V`x2HL(sCiMh@|7R`I>B&;R^uwN;#eyY^E%6*S^M=qtoVQ8sT zXGA;F&S>QO67J1aQNtK^@LDFOz0nXMK%n%f_t4<}x*WfD^=9b3bw8l$J`w)%Zmmp( z*?Y=COzEUPAe%FD%Y#g_{47aEthV%YNOiQ|A8){h(#8MbN0_u;O>(Ehph8_DD<#*j zQXR2NB~lL6rjas(!vEWH`)%Nl`OJaR(W}P6D|57(i|KTi=>o%YtT;57(hX0@mwr53 z^p|1ap0&KRl<(xMM=umji+mX-2GOX>6s;NF**9%smR(EwPvXM&Z08lyY7oW#jQL^n z!?TF_QXN^k#(=+7%>&?LR-e@sZ~8M^AJ?=O5AdI~#ioDoziA6%>=UmH!Y=7{GaR32 zqfuFbkjA-DqwMR9FHxDi$B_&)avMa_CBoP~S#f1=0Z}# zP9*P)cND!zRF*ML3I&n-q%_{~d()qpjRO%B&Il#yMkbsyokF|apORzxwOy!#2^KCY zj#SYQ2|7lkY(_N#TBRDx8!BijMx+8<3zTz}gOQJ*jhXr>x82NZ5$-mvbUA7~A^_t0 z^2lIX#m7$_6kGj5^q#MhIdNTE)7uOaWl?WfDf*Dwez<6UlCrfg#!&@y-MAkD=E^Z~ zdI1oCwCLGSd}~^ROT=at_YcB2`-*=U(k8Fjx~I^Hkjq+GmO1T6pt$REWgZl0AI(q$ z!ugD^sz0S8MsbNoRdH83teq|TNgY;3;LZggR;10&<&oigA{sp> z8lAmNCjg|L87-Rhe!7g6@Y4PI{*KAxW1oL%H6Fc#O1roXL0FkX$JX^(mZz2=3d z^Z9W1IK~VLWQ|C*{m-RQ?%YV)dpdjEY7PU1_Uq)*>#02X3%k+9ULzW-{NjO%oqeJR z=0A%h0&Z|agG;-gRULv(LXH?lZ`Q`)M{u$zCb@|C6VL7i)RutUNR@JvsLIYpVMdXx znWu^ESZqhqk=8)U@3>Ws%X)GaoCH~r!*9sw7LQNnmO_cbv*rXssh55u887f?ktU=E0-KZxtT6cKDQ*Y^%+vH`P}cYxQ^T(-1NRe>`fJ> zD^=@dNfflsw6^asbq~SAAW$&(a0HW^szbf?5dUbM4u#?S2>CRQ+U1y(+dADhMN1P@ zhGe|+*ms6?3%5FPH&~)+T_GHZ3Tw>5F1JLoCLb&96_MX%k8UIl&-k*xt3Sh4o@aOPny(i+(|2%N_@w6x zW|q`uU6CyxOy|=T%XjXA$~&D?%k-Bf_>a|vZ``zC)=@c9ksp9|A_M2N!Y{Qx8`hxF zr~dodHn4h^WEpH4ztkM?P_I!I2HmU6% zK)duA*P46(HbWi!t@zx;7xdmtgJ5YqA(?5t1w0SwQpDyTxP;Js{@!@tYOF%7pt+=< zge%yQb)#7#?6So|Pg5|m`-OiXPucgGPe7Vb!<0hLHhYKpqS8EN2+5Re1nEFM?L(&> zj5wBEdpz}N@kO(w^~(JS`a(jn&Sc`|NRzo~efP#>0}P;;=>MY9w=s9l6Vr{Kx!3v-EZm<-b;8C)v5L$MY*!m~O}t}CaHMl+`|k`;pW8mU>%!|tLdsX?>W9eak8;J-j%Rw(h%8{yLpTx_)|*D@VJ!nY##L0@V{maV8TIM=-A zk#@IXBV+m|bIyqV?UnW0>rZ8CHDpy-fkGJ?nVB$$S_F}0qQ0iNUfxrD?|OAD;)G}YdEW= z=FC?l(paoaseEZZbXAg5k?VtFqm7a_NbaN0MbR$;J>2nQWvS5|MUyYZ(!`3_Z|Rs; zl`AtXrpL)SmNp|?C63R@W*=h|B+l%p=Tr>$6P&F}!^t#ml6&_l+Tmp{b?(F5oyP)z zeJEOqAHknp81Xt-Y`)0*;KS*%1+%E< zJ9UEPb@-5;T^yJd;%N0?dcXVK>ae%wd^t3+|GTqeCSVzA)k&NJ#moOKxaXl$FwFJ$% z`qO^9pMJ*M92seAIo8cXI~YDwl3b0aFMKbz@d?`9le&@4c%91~w`vvc+C7Q&uP1O0 zf2LWy@?v?JEOHyriha}}8s(@tVg@6w%}%j8Z6E~*1>!1?6QAgt7q!}x1YD)|h3Qoh z7kyq9TcHESD`8SmwyUk-rOr8;7&y2NBHgvFq4Dy}2&-!g_^E1Tqvr*D*{Y4#>~8m{ z7ELl#X>ht2jD^3nO%I|%Lf=z4}=3a$N<0lAS}gTA+vTxQgn zM1~<3(<0nX!2`POa=pHCALzU52||`&T>$U#vBjS{VmeAQGuixi(#lMj{`133PM=}i zhu*5%@)Up)Y1wS4g9_fwQY>2?Urtp!SB(b~E^z*UqlzFtlH=iwc(ko?`+PoX&TS-o zJG`zujXnGqeHpd}!-a`TBG~O$-2cdRtqSpG0OSuDQlMujpg~uxBh}MY3zeX5e3YhA zZzZq~gYMia#VO3Bi}X$JM{?t4$+hI2K(rB?&*fr-^BO)kI!&&TioSqTo5}e#QqYSw zk8rt#2aWh~K>Ju;^osZqzjM*G*7}((bz)5>{6ki3r@MP3Mf^aC{k`x#U#Jf=wP+iE zp+n;h4A1Hd(^4Jzr^r{%1`~Rz4Hu*1vBJIA*ipIz^kXg~_`^2oLX8_Sc$>qh4h_bp z2YUTy3pa8Pm%>2eA7t~NW%CYuRB`5f(A1I|@-7}vlU-N4&$~{H3Wk|Xoug%RGLli2 zgI(`3?1xMOB=Y$;ir88F=4H~!tqqE;&RP~87>zPo9eWDsDl8L8^d`=XnP4flw*9Pi zTI1m^c9*8R9M6to3lAY>PH8MYo!k}(T_A#Z>?sv|P86nClLrr*m>#M=BK08gj_)17 z!IUaRGJkS3%%BS6ctEW4X5&PtlDTp}OqY^2-;7CuN>ie$!WP&NQ|B|G6767T%#BKP zumr=7)E%vj2|sxnVkJF%k9{rY57@Hdw?J!<^|+@&zI9plOf7ve9d0{hHB`D8upX=# zSY*O0Gp`f9&rGa(NWW7)sFZG>XoIu{XVnYGiv zC2qx1Z{aeEq9rgD=V(_!)$R9g`v;Gm~ z(^S8Rzgl{wI=r{ppy)LEbVl7|nU$#Z;?&OT$1Y-*mTUH93c)=-m2ef5Dd!h0DrmMG zM0yl@tw}ChUZyR6p{CUv-*~NiBI{19wRJgWE5xT5wC(nKV zIVILv?zq9g`RIaWf2O{5h{SdpoY%8&g-4ft8||W~ zvUgnN*S?uA(mz&=w|?SyXRi{%){2vRDgJ10y0m#7%D3O|&A?xtuI=!8uE*^a-!GaI z+r_@0F3e?8#+H7UCzRX?qA=Ax?_n^39gszrkY8AtOJLtmP{t?M*J`Jhh2I@YC~~Gx z25h)_a#>!U@>7&_nDBbZX6WC^+QGvm6=@%5o;}uZ**=Z$BGp)Fa1g4kF-EE48ot|{ ze}8f;pwi7{-i?+mP_@vC;6J#^#5KDY%}^^1qu50jVtnAlq>4o1Fo zC<-fRmoeNjfmE$f&PHMO=11XO!W>i8(EKlcSL@uRUbeH;D^$X3fI@>PMPj96zSH1g z&Hl&VS={_{dzUeJ)*F7|T5{-n3@ZMq;Yd@Jp-y8*2HL9hM(S(pKjO61n2udxo@i7H zrq`65t<}?MhuaOb@NHJ(FY?tk8x>;@4^aTJiVTwsD{?lg-i{l3#(nAxAc)3SVKX+r zNiREJX%E0Wbji2mRO(KyR-V@E=?XB}wsF#Sy)_~Cg%yc_=T}IGn562ft<7dWKv1i!?9m}Ts+m*J4p0H;xD&!v-O=9bVaUD0`Y5GeOY&k*I0(c z%$Z5LK2&o(3_`CqC@Tt-XlLtfYNs}Y!zlmd0&I@0HrW+{X&qD}v(wX8E#PvD+NQ;m zx1nA)YMsMss#nf7AW>n1Uv9+tgDbj_3!gK0NN#AAuG&+{d<$<1pi+O54yhGW=9>8U zSUeQi(@+juJ~8*XAhv~xBkydsNjm&_X0OnexRYsQ3Ad4XY?C~PO@}W_!c0KSr(xpf zHG$RhNJJY!|8BK>aD=sU49R z1=Yiqdig{~^(nvA_RR*RL=apO<0hh^*zPNF0bK6+Y2pSmb}XDAsk7`_{R!P^WL$+? zMD1i`D5en=Lm#w207Zr)YYG~f*U+vB~N4r#_Gd}AA%@d~XbO3ivo zPfupdzttAVJ6h=Y_UB-*k0xC`JjnU}TZa)ttf8f41SV;OqLBTE zq><;`(DN^Y0vNKk^_NQxbLDq`0iX>lgWI~a6Hr!`heWq7Uh3AB7gknQKBJ_*59Uq% zvoVS(6G6VPL`Hp2f4|`c#6j1JSo)3zC%OFkChzS3$@Qzc`s9mo*3el-i<#@i#6*gh znJ7$7%s04y$tN(8-~L-DWd3a!TyOr*J&;O(i#Y%JiX%}AE`I;}bxJ(;zeOaXvHy}i zV2D{`!9`$yc_05D4k=j#gxGO>eRla75CHyX4|8smy+1TGGz=^(LOVJ-I)(lz>JWEP zG^w=*HzFY=b#QT+cca_^*uQ~1z*HCifPjGiwo(DNLdL_R4gD$3gifuzHI*X-F1rY6 zvfD&KLCFRF{^K^Xwua~Az*oZSM9U+KBo|Q@|3m7S&d*4%Odz| z#G=t+GVllp>XH&-<1&#Ecw+Thu6O9XF&XXi5?k zn)Uy61VeZ8eFkO*m|)`uEdCq9Jf31sZKYzhFnicXzk#Gr7pJElAFxB1y;}v~FEdtvULOb7*&MNS{@kszjKs5h)W4t#EP220J9IV!T z4Aur_4ybNIVI?Z`Qgx7e8-vVaN}8IRR0SO|ePQCtJ^>K_g8wxoA{aA_ zJ`jy$xoS9V-rJJi;r_AX1Czhtr;qr4Ttw0y!sSXt=4c)f)-jZxLEgIVzqQ1l*TX1t z=KeAlT<@=grHW%8&&wt@>=9aAQ`&GQ7b>NVJThsB?JJGGRHOppgG>{ZTn+|x6gGQ_ zIL=rkgztYQ6dNr4QsGRwLt%v|c>$(0??FG=0CK&g@3=vC=vhB~9AvdrMC_+Kf!OR8 z6kaXz1)Z|vl`}JYR9O8%1wSWP)NindNTW%yvq@ji;NO8x(Ge^CR|ib4*1X&Ppi||v zSKNC#*s#_!D_KK(Iq;Y=l50yr(d+3ubKla*eS*Eis-Rb3|v9bI}>z2+$&~FtarctR*E|}f+v^G z9ixCmV2Xc9bBmt!>4f#XDM>la?G45B2Y$Zo=D7nNFj4K2r@%%$-3b5y%}16U&m?$6 zP%%6C@IhPh{&3`iaT~+hcHmo{*2Dv|`?4c0Y_MZLOaLHT`GZL>p#tP{5!FJ}_~NzH ztOEPJtOXfFb4%Cbeg_NWoBp0=^r*-dR?lY*hfZ}Oe1hSC?0#%Kl(X`_#g@isr)4=;Ha6FWPQ^H#B*0`d8$F$vZ-N6y5LXRg8xaeu`pBQ*ag_&*Dm zezUb8o&(8DL%zS^LxG3OHG~-9iGOrv6cEx{VLpEMR#ZBeL(K#JE;cc>7$XUn*MOL<4@fGGRoA8{^EWkbCN;_p_#tV1X0;7R|8 z*^WODSI6lc3)$vgyX!4k(lf+A&t}bfbzC{2Ay8tbRfjh!`udMcZLywDq4UvIP4OKc z%)TeoECNHj_+0tAm@{4}T59U7b37%Jk9@jVt7G>wS|O^W&Lv->Qa+-tH5Y zB=qibC|T@{MuIziJ?kag&#Lq3TiT$u!*^PyvIx1o**Mp-cUSCnL_OO8Yrcqi_- z_>O=gthaK<)p>goDtQ;K=5q7r=C3rpK+-YeEUL)Yz#9T2(USgEdD|mpej6n()$|&H z0?&u5M?@RR50~R7Rg40=mC*@vHlO#_FY+V0o7;jxD5bcy?YhbtH4HZ*2}K$nLJ*u3 z8ATcl8k*nJr!lODes9eb-Hq@^v1bO^YJBai|25?AW3-t$znB)}LNTw$W6gSjKL(nE znFK2LJbytFpsKd7!t#|VDa39myJ+;|T(%2&NKfJW!D6|3*}g6NB61|8xhHN4M1!l- zEx9{LB9)U-YHcS&lPKHXOjwUEee^OK2pPfG@=H5i>sJ;pmG_HTinw9>yAnrqEPj1@ z?Xmwc8>9|XQ0t*#3zt%a;adw@M@%$6*$&ekQBY%U7@+LIQ((aEX^@jIQf}pNkXQTs zLS)hBSgs8)W?U)cAamp$RJTXRqpJM#^c$loT2F#Wl8t#-di_xZ2nc=>;AOs#K?`hY z+$`iZeg!N?ceX58xEE5CWgJG;KmQV9fu2~uJ6f2_NEDji=?Uz4R5%J6$nRWaDW!Od zb47MD!Do2k*d3@iuk$DwXk7tnF_$2&cr5(w7D`vqPg5>b#aPYEG@cvHWaoXOYsK_Z zrMKZ%81%C6u%3v$Eq`j37dp=OkN5}>e^qmZ@ogy2^B2FVbhlnb8ftQoCKt^VQTr72 zBt!}F$%84M=X-Cs`8A_cCv+CyyTZH7)*Ig(-CO zn30g-%#;%G=d@a_U6qMmmA zK45?=v2ri(JdP7I@~3n0B@jkGoQ}DcAzC#o7h;#z00nGW0o!{QFw&8W11zAuK^pHR zI}WXMMzdMG>vn&1vY2%?%SrQ=*zGklB;v&k3-8N-)6UxFH-h8yyUf!p-aTD)Gr8Ul zcB8*;j(T>lRyh7MvdkILX`kvphyB$x}1-Z^Gq#3XlKU9k4s>)=H!y*MPC)f-m)u+*r>FdnVA9oHi;=1sx zW~>7(SLlz4cW?VOIu0S51FOx51S5IPV8x?`sZ+Xg~#*oq%^_($w&>O0~}^)NHb}$?gDijV>0HgVV(j zp1$VEjmf!-$&bd>blj$}Bm3B>G{!fD`3G}~1@8;>HR}A}C7u8vVBX5!$&Y6=gU{FgofE{ooy?{Nw zQ|(8I63x%+Uudy5XAWJzzWg*k2Kz7L5j>CZaqj&*MgLlC7MSafS?s)A- zBH*OTuDh~~C<#lp7KELV7UVT%SXatA|4gZ@ZErRv{oeM?`vErKdsLp5Ma_t4Jtl+k z0^GgfV+w8lL^V*GnBrw2d$bXh%sdoqkF$BtX7C9UHUZhcKX|Q`+sRA_c%Qio&YGsO zRu3s(nx3x)o|f0f(Ch;(=F#bqvXKqHAzaDahcvYR9PVqUG4^1F0kX6Ja zR=M+%dn=_{^@ZR+j9$T&mvs^85f`ym(E2N&0L3tZ`Q*lxm#1@)XXhXOvo5n0K23mc zsl%K2?)gpf-ev4?c|DHu&Q@iVblx`{tb!xYR$Hn4&HL#3y832iDYKEe4!WSgYdcAx z!h^sW(l5ewO2q@rRep~oelGJ{){?R zNg^)4alJX}ZSi`=I**Fs5#>gW6nF5^)mFwpZIMT;Se}X=>0ZLgNE!cXjEo{=r94|g zoseO?Hd~D_I-8gpJO=CQmWfzP={xg$Y`|GPT70#m=C``^5F4yfSHRM>C3= z`nKf7S=k{UK%#1`){L#J3G|u$O-?H(H~59$>ke9KGd0*P*!h zH{Osd`Vyhb9b@PKPEqGSuLhPfckxFHs9*1rKTfhE*>&{oAd??wyZj5U>9@|4p@A@C zSH5iih7|3zPgO;`;U`3eR}RXMboHh}J8gt=E3FACwMkV^4>3U)E_p3!Ty*n^dG4hH zQ&=hWygl=d#3$@Tn9=yEPjSX_qR-Q}+P?92H&`_uW8_Gw=si?}x zCJTA$WtNautsN~%^8DfzEpqEXLaHnZyA_^D7A9BKYztpM?3I{Ys0HI#Mh=gtl&!;c zlr%3@9zR^%@nT`f49*Mle!x+GgrL0hXi^y9A-c-$SY;ZZFY~digV0}7mHU&-E`=|w zx*$`H!Ky#xhpgQV7_d|p4{)iYN2v9&tnMvi364ZxRCDc8*x3ZS>UqCOME8{Athdl1 z``_lNWFiEuu7^xG0;xGoaMlRW+Dsl94-i#}f{P5g3vGSLT&|J4LDj3d@()g7^zw6Ld z68cr6FIlf?M1i!$irUZt0ulv^z0FvndU@?=vK{ydgO-^|B`d3N;o!h(2G3csr#5Od zg3J8ywE#ihs95Ex5Y`r&Km;miq)l}<&&NvZGBDnlZwD~8(| z(h>d0q|G6_PO+~Mf6Dz@L$Bd$T+Jev8f&d=479r(upm=7Eq;XRYFjsAHveUgZK53*Obf#SR1=quKrvh!E8}B0`G*pr;WgDV5B~R1qUplUa zjt76Jhd8abCE8PQGT&XKwM}TpqweWBxek8+Y;6Y9V$Gky37M??x0+U+Ha<^mga+S1 zDb7Tj!|Z;!0+4Pm8FF7J9HsN7%}X7BG~#DEU#|dq&gXRS)#Gt{r1pDpxKjLq(vVN9 zWIN8O!%-$R|2dHgj25m~xp&MgFY|7KS!ZFmx;2g)f?)enx72}&iD-+lREmNY@}15m z!M@g>?G?TwC8^5nys!H6nipA-$FO*9Ba=^kVvKgz1yZQ^)%MGEu$r2hq-1-XbRwN# zW7lS%rx~(|k$4(1d)5>jJiIst-B>+yu#YIs_{0P<7gwX$n60DZ$1a%k){I7*M5zD@ zyGTC9lx-`#{NuwRgE1&v5K8QDn%dI@#VEHi#VGx6&in)WQt)lUW*J!V@^KTk?i&za z>o+M$yzq~G1&$l6|>__hn z*nc3{+YMMIjgkgN!F(`raA*LTD8=D*(%{}bQ-Wd4tYS4(d9 zhmY@s@AJP1_zZxpQ*H@YFiG2Kn4gb51ph=$24y{JC}&tbRqo z!I1`o-YU$jXRGbNB=8aG?tdZ6GbuhYvou>+8*?Fr_`#| z{CzGdIZ`!JJE6p|9mI~AtNTNE^{^J4==Cfw`+NT-3CJ5Cqk?l3+H|zF^VPUNLCIpG zN{SC=w3lfej?70)z%FR@2c_@f&J+Xrj?q~Z@AHJ|XfcMd4T}aJAo-g2fv>QG;Hum7|P#i2A+$W>yJZS_RRx#6V;sJW;i9fNyMer{5H!->+ z&}^Ph_JTIL0j!}jH_)t)Ygw;L9n?6eIF@>DVH?zO7+yCP(r)@VY&b(r-@#3jVzlzt zvSYv`8qM{n*A13mSgA;in#L803BO6OfQg}3lnA8OIBGS2A~XjISrn4Qwj!ce4Fscb zg7;Z_d2FX)3Mj~G^%jTozlwYnMXk!@Y_Qi7y8lWfsV+U!c%m8%?PR>*J%7Q|Ge?Kj z^tO&0f{GOHUMzmL%;C%!ORy7!MP|bKhch-u@UxT2jE+ZE%ik0-ORvmQdi4^n0#!{# z{PH;kU+A(z_sqnysixl+WzPVza?@3Df!8CNdB%ft za;iDs_P`k&xlDd-)L-aEN<%;AE%$pWtL@f>x7$*zi!11{cYBM4fK`Vj1>rvzg#b8+ zwC~?x&m^v`#2YhFJl{4^--Tmfu|QpmHO%JlhfGr_BB6B>dz~55CzU2A>q(sEI~Q~6 z?ScbYXU8nt4P@=@X*+w%2&#L!27J&Ot2px@>#R zWb0#k)>p3bA{PXk7ltb=D-WMnkS6l7?+)rtdiZ_@$OthR1=)^c!w>)TZ8%h2U)SsU z+i-R@vOgAW;9HFAk+w$nywk8t>^eW54`{|_vl4#eJ_L)`)yj1^)~_2K*1D&sRSvv^ z?mI@zw#EWDPQ3{g18yy}_UgLnefiBZFU-ceqFyT%rq8V0>5*%^%I%!BVVNfPY#HfSF%2>@-j zCIR@GkM1ycvSRQqb?J;hquOEmhagnxR!brB{f1NC(dlXWV~ThzOA#Ue86hj?@RDj$ z=%k7ERmA3Wajnnfb(e)SR_8LEjf6OK&0gJKb{ktU}fziwd7>=d2*kA z>$3Z|s=Z>;eAozROQ1GQQto-_#~yhki-`rnDOU!j>Eu8diAC8&TERUUwhh!44H2zh ze;u?bDL#{UxNltuGaC4`a54ibDsH*VGX^=An%3#*rgth`W zHi*?VjR6`f~=~3Z=62QZffFrw5okzagkxt#QK!7^% zqadjnb|)(MsLh~dwsM3Mg7}rkQbXRti3>R$Uq&Ujm@9YR;UIC@^+%n)qrDYg7Uzn} z$wbe!W|W{{@WrEFU5QRq2}41(k&>{STx!*;FbHaX5O1`?_@{Il$LGr!tOewn?dUE~ zV6$qqr?icyoAKjn`(wzVpbqiWdM5?;pc*lia^QBi$Oh`?WOEsA?MtCfNdMHWb1Q-! zf-k`t0?u0`E0z3{JZ14Rn1b~wNBG>WF~xzrQ@zFP3i7RNKJ$cxKHzev=jQ=K`dXsf4EX&r$$-F$cLf)-|`PeJoE`|zLGFK4*Q%v~%O z+&XP&u1QRmUbD8LbU11_P6Roe_D_1e7||?-8{>#D{}&g)iJ9qcvGy|GRyh0d*70s7 zxLCbd2|OqxD5C;0YI0Xo2#Jt?u!|ycu(H}s1FSq2foZLk zE4v7n?A*9ob53n#63TH_e12w+<1d-CU6CBF!yUaac@0JSh4itxmKaO3#Sy zORK-K5sUQ7Ao|krrm`oj_Rl(`o$8v_uTQrS|6VKekgu z>2qH8caR8Bj+(iAaH@^HMLC&w4{7LJ6ByL~$%cPiGj-5wzP?+6#S|~rVTBQXsC0T6 zQJ>7_k9s1D8^<=i+xosCwE%U)MA3;qD`|-7+`$C<;~QytaesQG?V$m`4a|Db@Dkz9 zr0J(Ln1;Orm*azu^heiHBB6w^mXQc5`C-dKowsa|&RS^sy0|;9s)a!nDx*0Pgpq0v z93vyjl4ujW&Ig08#8@jE>?Ty5i{)7N%w8k4rTRv8C`S4Nc ztYfmHgVyLY{qQX#gv$U|K1`SVP2|zC`{E8t*NI8`xXm#eMREHDb>9Hwl#v|J~Wdj)IQ&L{i>$QQ=t6A-wHxY9d9 zfUOr*p3(S5?zTsCwL41SZ=rW#!-K{uCRjYitq(Xj%l4kD{K7vsUvaV2I&UO+>0SEU zOe2x%%0EUB?ZKl2SnPD7s#}o~ZRI{k&2epCp3aXX7uWmZTw;gyMm*Jojo8}w$1tSt zw0#*Wq=xeH96{IjTxqM<&i9S?m8!7F5IhFIgbf&xa}TxyoKn$nU9=LU4pnN$#v7 zYAGFf*@?Y?4meuM&_L5w@^#bfZ{fRX4B;@6;KchuEV2%{KXpKIOHc`Ut(|>hA-7~k z0)@j?l*KzJwWX2}pe-g=qNw@1(UT&)Di!ZT0OmT-+8|t=BY1A+vAizaLLtAJDLGCK z+EYGOm;!z|BV{5aFc62|56zLr9=9|lP{2z?bm9}sj=K08*1^m4zp@Y~AUReeXzB!= z(+QA^(GJ9_w*oUcu;#MfK-Fcrb#in)n3CbdWTTC82WD(f>g5YN`HvMqK^g>Ai$BhN}fckKbYPCvkfqXiH znMmvgM@;?GdTt^IJL&qUHEY%uf>Ol|v&0aN0yB!Fa)!-`8)6eG_{vXS5NbnN5p|rz zR2f5sXo%GfKc&5EAYznPB$bn+MigU!D;BRl!0p*RLwbDOnSoYB2L5B^?rEnQXz()Q zds?8+m;7`((*^*Mb0Iv-IcHuBj>0&FvQ-TI;q{?&Mr<&4Px$OR$WLCh?7>|Yb9d@U zB$QndL++2zrrAg#o(QcV(PJ=5OfsDfTG!%q$}^%Jw*NNggu$m^7iDUu&ARlE(zehW z$o66U-4Xa~K-RafyP>7{e`dVRXKMWP%~*S;(LM18>Avs$DBmsOq9Iq;!<@5TTZsAs z4{&-f6lv4C5S2O+pQU?!lB+)!;=Hr|PWQn}pLR)i(!bv~O;09qKp9g%|46QbgQ(`% z)bqGeAg>uOxeq1EQEb;SmvDjklnr`9Q*+<#E~ zJG{c-_KK9j`vVh6szc!IIqpC-e*o!uJPiakR$sHIwbn(?@y*q(2OdUOu|10xZ`NGW zM)BE*dYeBU8zI5G?8>uyO!ij{9qqrv38y~a1`=9^lq-?8R}-Z%H7V0m9|~4G;g?!$ z&`L4WrYlA5JNWB2hiK-X*Ly1z;8R=wA2|QZ_h7WAaxQ6=U?*z>!3RU(ioRO8_H%fI@Q;sEMSF=&A=#*w-nz6?QZ-*L0BKK>X8_3>pbU4_0 z6F)s%rXEA?({XEp{MqeCLtawYT>V#TE$J0P1QCZ4?5@D&b)?!}9_wrsF7wCPUFs;- zX5#3xxDzfNY;mS8PXY_zS|4&%*EK-LYe|D0h%_e8i6NGp%sUfF)rmz_rBW{@mjkat zDVFEbEM#B2Z07Q;MoqJ;Wa+IodmT~gtaJxiI7) zMvK?|F-@a6Osvq52_p|9fkUgwH0#%+AD_6&+OU`Mh>a8?i3+CynxfF`m%Hzi_iA*% z!@zX%YO^-QGOc<;Ci{~GaYkIEESU&_jH6Y0foK@uRx%SjiZF7M=dJ*U|5;?NwaaX2 zdsCzNbf39O3rqV6Vvgj5pb=L%+1#uHj+r_-m8F)DJANQ*ec#-8`sBxWwe$S0)<|_j zIf9+pOMknT=BT3ffyto{M<8wT%uJg#rxRL#Ii(82j{LoSD^7W+dND8e9`J8CXCDsH zdVoSCoicYc1`gTJCZHPz8Yo4W01!6^iRP-)RY<6X*|JFju)dVS1nijC=FeJ|A8$eu-OcLdI2? zdF|)D!?x08R9>L>LD=(M1g`tP@>9^qKNw$pee=QQBNw~Bu$oBf4?cW`HPoXHTMdN#7ZA$eV`L{YMEG#p+}VYNm} z{7hP_VY&E>XqIu_6N}O_sGcv_F59ZY?hk^By9JlNnV?9IBHa8N6M?^06+jm~t(>Q) zEdw0eQKJJ4uN18bQUWm~99qTh+(#BYJt@#E40qb=!H@U(Jiif;*yT}(PE0AI|0z#M z+yrXFMSB0C5gu(5=VnNC1~=~{m(Ew-@}$JOlkVOMf(S*7yvzxidq#r5iB23El>&ao z^RcRMUfT?WI~#@$`xG^28n~_VH53n~d=(;O{J9J4_zw1VO#+e?AENn+lwQcV9JLai zkaO7urJ`cZI!o7G7ox6$scgAhoFSqlAWO9fi;Q`ww)Us>bo0Ky^8AomeE601)e*NV zD4@7<0(+mRIM)HB`^?q8jR*&n)J}Yi5q9(Oqk4Ad(YHI}$}IEB$>_e9Q|$&5ypV;r zu7=U5*$TF>I0D@rGkerlQifjs{ZJXW0)>oc3gb{Zu*kyynno-T(tWA*0>ai~eY4*m z4ldC>=e#D@&2B%afN!`!T_@jgetsU3%W++9vw0)+Y^4zy{FkwRb#o&-%MUKjAznON zUU`029x0d77AwnyMp$QKr<~{^3Nu|+#3kktuLhF$2bOY1B*N6|Oq{JX3&7 z?iXq7HPm7_x@QyeU{5gTyJK}Qs95Hz5B7D7eNCDj=-{JoX^%r10=V$bc>;{E!Axc2 z-7WygHEicxbh~xy;6O40lL5TQ%_%qR%xrN#vGHM|rVa&L#(ZKn`U^qNz_AfMJvbPu z-|BJXgP(#+e*d^IHpZZQx3sb%y}Gv4$z}wX^FpYn{2-f! zQA@)pwH7bG z1W_=_h(xReZG$BwI9U9r`ED}B5sq99_%Z?D;%nJThy3VAbf%*&SlMEuNGXnp*?!&e`T0a}>6z=pDYF0Y-zf@u zaM#R+V!wb(IRwG~b59m=EGsJmqx1$iI5^oV#oKL<+T>AP&1TQZC1NS%RU|?Xt?BcF$(U7nISjpUB z;8abA*n};ZmmD1Yg+eA?syj63|77JtcXxFOZ;*V2iVKBt{;w-w_jj28Z?tKgQCH0S zi;c?v`4Ex>LHzMJTYg3&k;ay*f%sLc{O?}CN(XVU@!!7*nzy$I&cS4mWb|?tnF;5s z9R0(;C-H(s0%Y=K;$FJz@LRu$V6*;9Q*{5x+k-m)-vQd@zuNx+>0n0p?LV3~r1Xg8 z!HmnP$XDZ{Ja9#pe{uc)ac(0403&H+V>8S&&~r;5<2DT7hxe3D-W?^yh{^mC91o8e|K~b>gRaFlb+}h4Wv|9xx+G;X6zxXoZR>rvB*z-N*3&JD4b9*_u8}O^CpSEn~GR>~!yEqH~26+HER4Yh0QsXXEJc zE@tG=V47V&Pmk6)xK=LCb#jj#gnU11*4vm4`x%}WMf;Pk_GE{B7dK;m)y!chU0Ofj zTbKzL-2E1o-_jZ<#HrWUU_jJ1Y!=pedU7LoGW%P(k#4%S6o$dp)2PI!XTtu48xZ-n_bHs{Df4njwCs=N>11_G5RB4h}LAhXG+|B_>P>$$1l2z1m;qF zy=XGvEkoZuI*r97a7)WeztJfA8d~XBkRXl}w|m_|ivCD9x1Fvs?n-7mZeJVrm8#k8wFmYm9_GYIPt7i zcz~1wbuJKp<3gVBVPo<5or9A^C9sYkRqU(4W6}xCD@in&Ih^&Qdmpwbjz4)2n3Y_j zIq-B!{=SUzhXmGh!g^zxblM2Y9Wyq(RQi}M}cW=;LgbxhEi;p zxyX#<7$Kld<>X}KS2*X|Mk7ng+Q;@~FvBZH8y`bSQctxX4D|SFfB8ZT?(q1|?U=wQ z-dvnU1xC?w8%)o~@^}W9OnuD-4>v1Y3S0E<;LG7%MwZHt(G+0SSzlqNBZJGp=$${r zW8I+S{%FdOo(Z%jdqp5cb2wylW7XcKYImKM0>{!2+c}gm{$d@f|G<8d`jTvM0uXS&9OUt^1FzOx~uaepF|(Ur=m zRFo(7kIP0lPDoJiWZEBXJv1jJdStQhMr%Pl4o(vvy}u4PSU|3}8D{pbvlUBF*(+Mh z!OPvh=o6ZHIo-9@90njSHeUL_&UPSS(9Ra9XTj3*JrhS=2%l;$Cak^s&ZZk=&JAW$ z|Fm66buxzqR{G5HEAqb#1o=6B;`E?!-A5g5K{ZVwB6PKTx!a^vB8u<##Rd3%*@ zpM4|epc0b!_F?u^QM(F!Qg(gt8PtBa(+=$56I5yGgP;W2NiJ0}+?JdUWm$^kB)sH` zBc_gZ1e&`$N<^NDMCt*O_otF5vRf+S#_s~}S2la(6FnFD-CuF!hB-|C207o_sKkcC z17M}ILcPUlV{i&B+G)lhHSpUaRn9U(PhPaVXX7gfY~r|%a}?}5L#8oRn1V6ty%7t? z>o<~{p8}RSk++H_$QzoG3LDcnsw;O8w{}mn9|eXKo?6au_#LS2>8QE+#{<2gxGt$l zC7ku|m!#J!*7b}eY&BEITO!5(>!<`nS}Xm#Z(JhLG|c$L+4Wtdi zL^w_j23ITf4I$n}D}HtRUao=)E@is6$*TgZlyFq)bELvI z;NSpx&mZz^sl}gvuGTlTQ$e4KK2T=Tbd)#M{sKghoo>h+Z#dq}`Z4RHgZ1dsyOxJ@ zE)JsEYKO~by>@+>_iL}KG)%Jp)-NP0ISsI++0VGmmhZYXJa(K%B90xDIy=FT9vK`w zyt$9IG{i0z`BEh4k35HyrQtCh2G#y+?9H*zfQ;ehUnSK zPmSt`yelumG8I~7bi3gRD9dT5ZIP&^4-pR6(5Bibni;|C9ScP`ykHmYFMq_I%(JWl zUh2r$L7e;3PzU${YOmICEB-bT0HC;+|3o1sS$`O)BQgJa-#Uz{V&4 z@O!)O!Jw>%@~6_N5)?PXnm9IJ0_Smbb?&VaC1p-k1&jk_@h;EbJ}hu zjG0e@L=7C);kH{9+=%#pcfB#5YAX_!56agrioY$h7GdUIFj9G0FEL=*rou@nLZgw@ zA4#B78^EfbJ0Dz)V4JhWG01w?*TB_iKIFMNFZ%DoO?1i~s6J^q{~?K*h?{f}0I?qA zn&cYD=>Sss3IR24B2b`vgxuf=HvN6Q?MRtm+G`}qq|5*r@gYpNq*j1X=ea+*E(;8| zM@nm@T2cIax}F#Ga(zKZd6m$G`_-fk)jCC!nkb#ILcTTbLHp~}E@Duz~ zC)bpq?Vf$qQ)Wj6!{i4(F=Z8JBeF|c29_TC~@os=@p40O#G)*($*V0fb=nn3!x_IdqA?BEN z%LA&wFgL1)1xy4HFI$Z;&NLOf>*z&xT`+qWwWH`WB8_7C(9KdpC$s-kGMk;IDqJt1 z_E%M5`bu-4Z^(U>>jNw!j1R$ct{UZ)AYb*_@tyx_2XuivTC1b)@K${U>=(|rX%u;= zBuq!4u^-0{S@$8!r$p`;ugSXaMpKM<)#o#o4uKg^L4uy+vVSQv`wnk`v|$QH?+ZlK`~`4fJ)>{k zCHA#hT~oP-V;{W>%oZcsptVY&vfTIm4ydt*netQ6UFL#ORV2bxAAU8@x;gTViyk3>x-N zHz(kb1kVMlBd@P4U;J&8+|g?14)Ra zK^f{RLgyQof=?#`SSIyou)1>_95Tg74+J{!l~(A?RC_E3c*hX-sBDkZKz0=E2CbtmBSB zp0Y81dw6$=cvNrFt*CG@tIHslA8&8D2FTRqRAYiyg!}x;8Se27myai0o8(OS^H#6Gm}V9a9Xn zK<`2;Vr7NL-*z8Cxy%gXyZFwpCw1{%cO4!USS2eF9?g8L?}fr*_hpjCGk{m%dNdE4 zLBCE;j*^!SR`5}T#bu+HYB)B{pnf}VFk2rrwns1pgUyv?5FG=qdcLQDZP{ofEMnWh z)m?Z{j^ezWi#W6;oKwDakoAZIIHbo{OqTG6NHh@am77y2{iuIv|!pRKmavo*|7 z30;o!l^}L|@p_ScA5Wi{G5CT$mOzgeyR#9Sm5Lgb+zYdovxjM$4P5y}bq^+9*b25{ z1Fajcx%7lp>pr+dNB;4_Ut?ACHXlVj7l;tr#qT}j-bA-%RhBS>w-4Pkj?e}lwP{Z~ z09Bt`T5k=7X~;5>-hVqNxAJ_KlFE;0PK2Y87nWA}r`7V3`FzA5WN(G_;$AM`Jdhv> z*nUzC&;JLK{D_J5tvikcC*qU+4elvch1tOD2a3j@PvabXn>)6O3kr%kfSFXD8UG`f zQOlPapH(dvjS^8V7_8IlKJIo4l~jW?g-y?G<`6}_{oZ{By_X5>R@Qv#Bc4d8v16rv z-kpVp#W zsl%+cd9iiaT5i|B_{w7Du^{>v?a{523@M*qbB zZgP2Fh)@~3xtG7qXK7~E`%fsZk~hf3L}!%CMbRo?d|X`a#)$hSv#|R}NNIjbdN-M0 z?&J6ZFkT!_=MgQ-Xa@X0N$&AK$f)Sn$CUA~$I=*tps56>PQQBEn^Xg41wf3fxr@*sN{)FoWle=v&a7%L5lczQNWl% z`E&UHVJ?GUHYT_8cF_OM0^eVoJ4B`G{=;g-SlHOo4-Efx;pD7f_UY+qDn~qm90V@O zsIm@*B%NGbrUBStv5BVO6f||ALdN#pNl+$grU&>N#8~~{j0yOHF>br_|0yA^_ZRW^ z@qxJ)%;3X$#Q)IquQ9}XP=?Fw9|32l#5XAnZnVS)sbI=7kQfLAfB6dN3KEV4R=s_> zG~pA#MV8>j2A_N({zo+pZKAoj0i^BAwPN0zt1Q+Rvpe2!A@3SIf?=df@GU9%e=4eA z0yn}E?BuCr>h0){a*~0y0-W)@G|meIqf7{C|3ltl!(&?o?Jv_k2*#qN;SZM86m>p) zv5t(uz3ZX)?@C$*M1)egf_z`Y8`Zw?d2?Az8uv73HG%tZ2)@woe+1vFGt!Ia%{`tB ze|7+k7?xk0b*90qFBj|54j2cGxkfntF9aPi$0)Y%dLtpR97WljJljJUQ5#6m#K5VN z_y*;Vr^mmkG`7Wjt%y(PG`Qv9=FR>wjlpFD(VwjN0Lb>Ha(;U|3is!&kVWjCc&#nw zvQLZuLdlKuD_-Ik$(ptra&Qh|$HOgwv|Ey8{P*lj6aDYq<{M{3KSi>QMyH~Oh?4(B zUj9Wy|313-*ENA6Q71WliXZ08t|e(NU>wZKG{dSwVhaA$m!xR{r{>gCy8}^B!Pk`w zD5gIO4Sz^Nb6zjs&2%I~57(1x{+$y8OER78)-0pisExPGUMYK|_k0$w0j5wo>>&KQ zw-WU&G(o3T52UnzX0|woj#@>cEY}mbTCK!`3;uMVt+}%*N$2>aq?-Nb8Z;tC4irPt zOV!3pYkNAZebTO3pbV12LjYo{?9w^dWsM~pcbc$V3BsrD%cSl#r1b7LAYm7iK7K@966;=#FGlSXFXSzN=#wUa5NX#CYXKlT(x z*WD5iZ9%`Q!B1#8flU=SfIZU9*wnn6?bAUot(FER)UXwqZJjugVq6g7n_N8_Bk21g zJ_-Q=1nT0?&`z(wuJ7~+-_YPu`u>ulBPiJ^+g16Q0O7u+dLPTAB#$m;gtv+ATBK_r4oCE54Kv=b1KP^MqPi;*L~d*sjUu>;pSuMz^8ui2l} zYVoGidO+2Oqm1&B9&cO8NHC+)6soesKawB=j-2?xNm9cshYRDW-YtZzs^0>FS*+CI zA*i)nAN-p1OT0v4xV`yZaI|9o+$!vbmDCnRx2S6h#NZE3U*t?nRk*YPkP zz3zGR3(W=CT*{8yXO4w_)aMlhkhrG;2CSC^&;39b$t3)I$6^HH!v6P6-Q7L7ySuwXaCdii_u%gC?(Xh(o9~;MJ5x1t?=Olvr}pVBy}Q@5-?d&9 z3g+?V87#`nT@s>sNkgLs1Gx$#-r;v3?Ob!-DMznh@gfrf!#bV&{SH~KR4G6qynWMU zD?<)e1~LlETOy*nXCYpD8l~E*&&sSMiTZ40|65@0FFzPBqUOcZOWbrOvHnQm)uZM) zaKAbFmh@Ep)!0u9NeW%Hqc-LIW8INmP1H0jCX6|f`hOemF2;2~UxW<@%-h6Z*_lX@ zFEMUh9LMR7U^T<0r(vCE=ImkZES9SzqIZ*7r|zQ zb07VaCT3KEqD=fpHpKsOzUF1dYzeaf4dFmWwv(oyZwB*kSb(%8y{1LgQWj%ALhGPP z8HMith@RH*kgwXszWW?=xFqEGYQaRO1ComE-6m@(^5YMeBfEXX=lk=BYughO44(x~ z4TCIoaq|5&{*ro7iI`_ut_Ha4W%*v43#@bkg?rDIftR`b1@b1ow#$C>_t|0N=|9d0gqelvzxM>XL7tzBWceI!KQeQ@lZkJT2$%~dx(}x*XuZD zQ)|HOcSB_ulH5t0pNl<&ue7Mf1N6@!bh+{6VijAk^K6ssD1s zFG=g(8Yn;`xSYO<&cf;@vNe*;ww>h*?e2}RqHl{YS*I!@r7SwJdRkgrGMHj(XlOW@ zoACts>ioW8CEWBN@*S{~Ga2DEGCEqjFxyh9i=wA2 z`gAa1va7$uZ;Eiv&9OQkd(M&an!`rhA^gd$yWgI^^-fWH^_IepEap_`Fq~tX)rpuY zTr$VDglmA|669DYIf|<)*76ut#az7bk3_7e=iXE>&Nr^rKV|c3hlMECLgw?~{@`ha z^jg2@Qc`MaiyTs2y{XjH>4GNRr)n4R%> zoXa7PVh_uxppc4T;-z3IZ4M;!p`aRD@WB4Mcv&xXmP%YJGrg{HXAis{>qdL8h3IeD zm*&#JaI8U&KWL%gK`A)3V_t)P1sjJJRk#Jo42(iUM*i|6NHXcT@Z?njG730;80KwC zvPqu6F8iVF_Iwk~CbTd0w!@!XtJ>igs11EINjhf5+vY*wISJKa@0Q^asB{+cT-@H- zMn;h>*Jb_O<-g;_ynBs=R>5*!6>x-pstjv1!sE5-oK{_f(;43UdU~Hrdzip7^?-j=2a%mPd zH)DIOy<`9>_SV+h&PnuW>OnRG2b)u5t=3MyDZ|=)IKk znXpXIl=`T*ADUTn^;p)eS2`}(u&+n)EltIVjGApPzbzINBxS!#HrD|6UixuA) z3*HuP|6S+UOvkQlBkJ27lBOIgSe!}ugM0f7k%%}Cn+2A_bn}OtbzpD_G-a|iXKqM+>z_H8U+q!yn&%nIk@5I zy_BP8b$6;Z%C@?)7xskpm2kUw@d-1ktG7H3PaikMMH1zYZeEK`Zdgn5YIaq$ zVtE2K!8}6eI4OMx^bmhK59CBeoqo+~aefJevljP3bn=l+AJ0;gEsn)Kv^V_?Oc{b2 za=>Tg+T)V$l)4(L)YjBY!Pd$mcwHMYX>rV(J?D8J*Pz-UT#GK0(NuAS35uV*Y`4iRXnD)Bg+Ttcbsl7BPlSH58l9=veuOg3AA<@?Mv@plW|Fy z*&AH1_Ki563x$@%2~Be5~! zXtT8h5kxnDM_Klcg4>qgy1pBfgPu-_Opu14z+$o;1)QzINGFRW+qW4+#5$8dJgGkD zv46wA#I~;Yx~FZme~h}9C;>EA>t_5jQaY%n7sV;#cZ{Q-yrsUjYIsJvWp{b}K3 z%isq`uj<5^Qo+0-t9*WBTB(%il-yjQr@zEU)N9W zL5!=+dEKIjYHwF3;T;oJ9!bLv$(Mx-#4sB5PmdFalrzXSm~ETaz}g+D{c&qH)Rx7! ze@hMKWgOvhKnSTTKjCUZEC^_%si!yGs1ma)E2r&C~|*%$TP1I~MF zqVmaEt&^50j~5y|KOt})X)Cm&#Y9-_j=QAE5D4P%KRwwQeXDcD1n=nE#SFlh-o|94 z(JSeLk4Q;Ox9aacrX_NG>7DIYCRQvXm1iy6(Rt37BfFvl6_!~2DIa{?^*3HW;@yeG zv@TQA&p7nHs(+0%eRKP9B+2Ol+`gg0aQ|}jx4MfD_bQZJ(c&M2(;5WtA53>G_SQ>v z=w<8bN^B1Fej_QgnKtgPUquXdHf*tGheGU%x=e$V%|d6H?dPEi5dQR8@(LQ-uBf z{l_(1oTTxqY)EITFKSYx^}*NOhc_O?qE$Gef#5LDw|#4FP_-raa_4n=HKW^YeT~Df zz%Z1>d#1L?Nc#Kfw+vhnKvZKQQ}j`2j~|M}=6co?W0cSZ0Z}pv`dU@W(_@ApIZ=Wu zs6d~!2goPJRUb41B3_*AiOeN1CPq8TtewTZxZXD)YKj+$YuuD zB$H&i$!Hewf+|OG^qp5g<&l*v2jqVqz>EgD0~g4jCGGdsBIQsG_wDXW-H$sn2Xsj! zV;La&j?0tzC;XL@D?~EA8|$K1#5lG|qA^|nsUi{s#Knd5|Ne|v&JgMfqkRtlOtYWL z*Cz9|QKO_^nvghh>A+1f1q1X>Ml(yHWPz=QaUC7iM%r+WQ=u-F5J)tBppq&olHC<* z2clv>GdZ-H6!8Xev@u?Xu=3yap|Bt0(j--+heJ^kn+S!=C%;mDQJpZklpuY!bX>g;9Ic|X8;{j*>SJ4yYB4=_W_YXzE;XjIs|2H=p zhd(t}|373h---Si1N>FZh`WWbPQ)j6e z)!Z{CHvx%_Fdiv`!N-IzL3wxt$HfTJ_TFMt;H~*2ap-<%x1#+2DcczJSMq*=!INAU zAka+IIX*!0Xp}}SdbAzzoxlgbwl3FM?xO?69t+V}ejai0L3CkiNp|Fk#&C|5H}Pg_ zKp&I~1N8fYQpArxU?++^59w1M*Mv?}@wQPZ7)kefzzT)`&X&<}{pyx1G7hoQ1@FTizhMJF_k#oNJZUhQAlh*3@abllgs`B5PeEHF4 z{ZN)=+&E;H=pHEa{e2Y4IqZP_p4icnvXj+tDNS3HAdeCOP3TR6Hlr%VUfG?ur#)gsKP4*`bv)pGt3;Ch{=T1U zY+QFu=!O;-KXD^)pGeuxblP)Q`KA24qsSS}hS)Z7${-Orc)_cQ(ZD_4cJ7`uMMzne zTy#ryytfzoH8w=Qk#Ibt;@8P z>_FbHjswem(M`bR9STP#1Gj*)KE}aj_#ScCB_+Eo-8XEOl=|qNCs8#*>l#UOtn?s2 zB-#OFZB*z140GXXZaUkI0Nu$Om3UAZK%A><=T(_$wso%3KwZ+lwXc`! z&zE0K%Z^#+U?_Lyn7@tN<3g9*{3Z}j*352LGM&@rx{b%Twcs~*H zN~jgHZ%JSKcDqd*Mc`N&h>_iM^yc+5E>X+4vrxkfL-%=6_7Hiq4#v_Ixe^}B>v*K! z9ofgu_1-Qh=}VNOl1E)>4F;+IY&{S#A%~p^iXrfkG8}Yltl!%ZZR{UJH3zL^xXmwR zP9*!!#{gyXX+hYgMHB0l->%OSyIjNJam@=qgtVnXJQ+?9JH*nFEhK3$A|_pI_IfE(6X3}P6at9an_7I z4fB)Hf5Q_!;hz5u!B~nuIVJfyFjS?wB9tw`GK-%{zOHbNSSlh7ce*+uuOD<_FKg;~ zn97DNXDTL)7u`g7qZ@*?9x)a%6OodHE2| z)Nb}#ARs3D)@s=odHG~#kiPzkID~&o+?NQG_EAzhhujvw?vbVaAAn;0K5tY-Df_w} zOg*)Jdee#OrUo%*bVNS1(qiC`HB4bsE$wr_?XHtLN{e8Ur>BbC5?Dwv76>8@ZFg7p zADd1KOumw`uSgR?SLfquDm1>E1?ZKx$z+(fxJ~%Arb2u^5^_JOBa=o)m6zn$#yC1r z%FEGu4`go_t#m=vMTLHQP1WO3z|_cOUxV-JAoB()&CpaIdLf0Eve(%v zO@{D0Ty1iQB|0W48;tD14ZCRUDQ7ae7eOFh;ZglfRNp$Ww(wFpU?qZ?w8(7iW;@JBC+ z5-NI*0UzDN+PRm|pZqNijO|Uf3)`kau#R<{~g!UkP6G5{&(m{n6Qp(D39;Uq_t&9e{H~B(7~tj8u371TsN8u zA;5oLo~SCzN+A4{3<-H}GMF6pU+=cPB{^90GjNf9rZm;rnaJ>B@4Vd+Z-d9?lKAVw z8&O_Mj(Iy4=l8*^&Dg z>73Cl<0@5p@(@FX0Wl`$+S8@{8Ga!bg_9{fHRx7m3YQD|Yai@mV;*7q*7~$3x)7${ zSv-K&G@C4bdTi=&{FI&|nspkM^z;XI@jKr z{e)RU#KV$`FFYxE222doV(#XrVBOg~=o979_5D8Ch9~8_rk30zUjw;_CoO_Jdu7tW zs-!xD*Mi-7^$36M`!{lDP5^6}=a2l9sGDTu&k# z{uQ})eH`{bAz~0ytIA>8CHvc2nKPz-fDv~k8+3f#erM{Z5#}e{yJ7EII}-J8M(uG0 z)|{)Fg4@J+Q{LEE9roRHP$+ZS$6ZLNE(v3niDM-`>98%Ao)V?|0-EdBSDXi^ae;Hz zB*|)zB&DD{9^=kw^-)kbZ#hu&$J|t-Or?vS@y`W!m4qF>8sf!2%&`h$=k$SE*&uD- zqp^Ip22rATu^*O6OvdZ%VFg{zBafz~kC>xOnuyIO_&-og zQd-y77gSA63LvU*b`3^~*}uByvMzP43ZvnhvpN)q5{&iu+Z8oUCBsfh&hv%bXkIaZ z$<%O@*_DMS%tc!kN07`+^q7{OSf=mC#opX&p)8dj{0jxl7$wPlFe-h?jm~DaAlNCo zK!r4V;|*zfC{Ac4b+og3Y)t91Z1B4TP!J@yX*IHo%c|R+`3~=F!fDkYzix$Z8ty-} z0Hh-I7gkdzXzs`ytYc50aM$&J`&5zwn($JSY^q6elIwRaeLd)le)``ihL^!fWwcNTI9@#U zjgc<+Qtjp&GsIEs#7675M?CXs!PZFYH^%qHPGQAy#D&2(^?=DT=^xum;7VaiLx1>2 zQI`hdAtcl*iIZX^-};r=W(n%N5qa_Fo2N@tZCKCjamHDy^iZ$osPb#3*$uu`@lc6a zrhnUf36-EvfBx2exBcLLMs0Q@!+JnU=}g^l%2%~sVEuwvyQn9gq~vZVqmgjPgvlGB zzoaqk58rca*k19B zYch_^j)b|6Sk>Ey#9PBu51!7A!*I{<@6b(Hpqpb~Vqf!1{;hRxDSn~4+k=rH%l-mo zB9L5t(4$}ft*m4C6*`gHl!Hh^Ugv7E>_(z4R}eBvp$pWa()qXU5c&eO!La@3*fO0r zYlDTYX5Z`Y8K)BTl$OGD1*a<_b{>bh!^$OJUP(a~bS;tYD-VJD_^4N;7T_ZwL-4YCy=a;^4 zu|ri^y1{t@k!WX6(Zh8S$S8-{tT&oxr@_DJ6KzoD3Lh6v`y!H@uJi#+i#M#k8AFmKArOkG35 z)9Y=+0C)zdnS;LE$LRybhJ$HnWCz%MBw6^(_EPX!)VU%&ZlqL^GNkx^T?bk1+NdgC*=1MH4{~)snJ_ zHg}CNz8-b#q*2!hVSN?PRQhM=!a!U(s4{~#WQ;<_wmqtE3v(~F?7Y@|5v#qdXZnX! z!!0{bmPHLna9svNJ zFDU@mIsqQIxZKp(sBL1B96SkJ;M4Wp-B_)?vOzeo5O9l@_~3vx^#0TUpy`8q2<80) zK8kk9sNBak?T1oIINMe{U#QNxzFbnUqbE1k5zDpEKOq(CsPzAcfnwDgKd}1XEnk6F z{^3J1gg3WH^%Sb!!+q+h7w_C#b;aH*`ateT2%ZmwU9Jkxtz95c(v06ldiVeYAT%$Z z+cAo)!U_3cUsQsY)ENWyikmnj zcP6$)OtQ6|E1bfKl0V-g2AZ89&}*5H|0H(NdvX1!$+hXjc@a@!HnN=-s4=PFI*Q_K z=_&jyq^R@x*30Rk$>?1rVr)vZwm(B0R`(F#ou=a`>&k6)?|Jr=j0@(AK4uT}9{~84 zqk)MPs6U+%#2c?8o_J`TV(KZL#n%$V`&)UUd@abBt-|{GfZi-vFjV=$DX3AL0?p~c z{-*#Hu*f)sKT7Vs^Jr3v7Zk11p1D20acG`?#b|Z`VuE18nn4z_-cll~i(ocg@oNvw zn^V}~lVsZMs|2}Vm5|U7 zvG!lcb{)$m%KORhbw>(gh;$%O5@#$Ru|5^Se;9_?75ocBd%!k1>xn4gxw_>9@{$gSj{ne%M5&LtyJD6T&>yBdq{mxf#< zz-kVe#h7#WVQnYD{=e|t zWYotUA_L&rX|WRJXH@CuXAn;FS7LWJzI9vEbDh4h#NTg^jng$*wNs$TOzG2hQ&^?3Tt;%QZT^^-%rZd#h?mk& zxsZ*r6-%&?>r7-S>=jvO$iyk*Tw(zU!H68AGNi}(_?LSh3>NpelzFPnXL-2oeYVt9QKW-mB)t0a1F?UZuR35q#|q1eZ>>C@5BgOfig3X(fg{mK`3TimHfi&XpOu3#m{o;2j+ z88_Hfe#g52}nPeOdtgxVfVmtLq{6ZDEoS7T}OA)xD{z3_x( zeyO11OWmv5d@xTCwWm)SC)|CH%hMHY+Rll@`ch1%c-v%iE(|7jGrMdxWNO>Bj`Os=gG6Geq z-I2zl8?)I6uGee_K2nL4q!^><9t&apY>zVa6w;GD+>5;@`}E1k>_PUB|3Z}ERhgdZ zTNXlcd=zw?P{Y|i;u%>BBd241)0`vqRli&X`|*JtQ+861Cybrdnq7WKn;VACoOx@w z-SW?jDDr^FVDNfMGgm?3w+Hp5AndXv1v&#w_b>XRr-gh}uxjrCNw=i^H4tAixuZI$ zmMRqs5*PCr#2EZ(`?LvdDOqW|uDpzF)({y#ptwCu>TP?f%tE`evV4n6a>_@t#A0(&!PQle%{NFdP-t# zh!zNj$9qr(w?l_x8O*N#{AQ21=~=~MPn(YhJH-^-e%UL{Sh?P}&WgWR_Zeo3&au}C z<=4z9SaGIGQ@Q;hPl@lPk_gN5>IqM==cix)*oJsBgT^M0&pzW7M*D69+kM2y!wG># z(5MsQ-PvcMh&Egy1h(HH@4;01&pqvM?jRgcdm*~^Uc9lOjCc~B)&sXy*sHt=02>bO z<7db~H%(qIZ^Tfl?b+hq!q)_5=5p9agr!vB?*7VBe;;U>8Xe;gY zc_|;RIZZzYK>nZTHKnEIoAXY;ex~uQ&18n*m(s4aQjYSs#=_Y=Du=L8;&}B%)HPcd z1N371=MXrg2Ov%(fCUJTh!DxSeKG$arh=RKYfT=$LpfyP`{4L-o!RWRx3@FMs$xPzhmkNV5zc z#t>_px2|)+;Xb@T3ZC=WU+Q{}p#CITa$4srhtRlS{eW?Tx6fCJOua77|6qxWKPZ0R z6;m8qazw9{3tb%Qqh5{$SgLXUO%CY=r)^JXynw-;pS)Sp^s}FDm?#?=$8mN+=nDr3OGg~+3034bg4t@rdu{Y*jI$Onl^f|knk)HY}8=fxW4}sroFii zfw5Q4zm5b!?I!iDM)NA5QVkm?S`tBAd;>Ly2 z&1-?XiYAd=FH@Rfn2mpgZys?w9Vbw(=f)cnj8F+V3+n zq%@1xO>*8;XTE%?#GnpC2yu}jQ(#)0N=%9kdjnNlM7=`Jal;f}sQz;p*Ih+d;;n6r zNk1D?RpC2E6!4?}JFgxqAS>-TlZb^rW;u6eqlkn%*R*+`lAasI4CBi|7~!n+Ym4)! z+sASy$t{3*xTGzoK|0sGhSO5WhX46J#OAe zYR}r2Qxc>q9^}=ZiSG;AJtsukFNtaVS@KCPeZ~&nB>3|}fjeaUS-ftN$QoQb+VI5J z2TTRh?j{5>geSW+gi5m;L)ojP$CkfK)mSfZ;P8m8`ysTO9Y zgzZAIVVY?R|G<>MY3OJ%$hM`eb`L?s|2CCBeP$zEgsV__5f$;tZrLUs_jzTZt{jD-i6fW0Oyb*t zzU1>3kr2s!F3x&xuwJ`xkHp_o9CBeDU=G4*e%OW92DpE9bMn7#lhht`cV|gJR8V^c2{a z-STvBUl_cIDx4EyrT$;c8y=%7xZP77F;FC{%Fw;El6WN|f*6g;7ylW_c*>of605Dn z)h;cqd&tVe&DP}Ik`7Mh*e#X?c!QgMl*EUY)+bZP92V^u-G2$LQQ9wg=Sn^5DCdgb ze%soI2JKpRu967$)+fc4)d8A;kY4fjh^bMX8E+2?h{@LFcWz5^ zq9dfx>ULgwM-~2OKJ|ksT6D=ihfIab6|i>ajyQx2W9kyFI-5V`=_Hjgug$PIsyX<5 zbufi&T}dQFDYV7LVpx&~LNP%4ciBS^bo=lFAgJh3$dhp@4;su-`^Hm{xS~grk*p$J zX7(Lw@7wO|YR=!kyc7k5a&y96Z6N;}g#I*yAp{w<2%OPgbl2$qE}V@K+ru=T&-_6E zb(|S@ihlmq7?@H*mO!3nb!;hkG^-lHvQS2%n{#7old<`S4FrJ&(=4<+nO47zYFyDB zzWmJJY^5awfo-pf%TT=Z=h&v%e0bs!A$>%KH@o~(m32@0#CObu~G94L|+tHKx|1&XgOV{P3# zv1%xYMe&KQ!2eU+brRj>2F#I#=J?olN>bAexK|TWp-j!E(IrD7U%S79qzFFQ^rJ$I zTNl!B^{?(BDG{^8=4OK1qlO0thJtOmC(yqv)l(f#1&ajx^f6{=x_ViH7r zybr}D!wdiaGuzE|r(@5gn0|I<`&~vy%her`Ia*Y7H0kapg0j+LX@0t=qUPr2X+Tfs z3uiO`<@&0%sOg+xI0Z%cZ)vb|RSBElX?o4rJN2q)zpQY%WEb#AvD-L34J#@#ZaK~< zugtgOf@}MvOe(%JwF}h08KYVv9`sI@aL<3KOS_~ofCCkVinr_y4We`|g>C!2=L94+p*>(KcGbT5?_9Vg6yj zbIMa_5=-`w=^QoWJOIH)7j`wjw!>9rG#92c4A*iL$83!u!=P7ZO`^@}tl?~={kAox zJ^MFTzJugVdMqQaWsPGmqfu3O-!6eP0F81V#$l`^V8$q*{xz2!xq%gP^Z8A>Gctp{ zue?Ax)_Ak^@nF?danCT0;@uoKC2#CIL%ox7^`eAz+OUwaLD==2Kr& zArxv%9K2mkl1RXWCz3Ue;&2&2#B668xK?rRjR^p*ybs}iGMQQLxrW9h_>J*#$yu6^ zdON0u#%&JypoDeWfAXO%l&FGB+rf$UuBBC%+v}b zPWErI*wsWCWcdVR4=>wS^pChAW*}&H#l7^ROuXXty=#8C)J(UphJ{Uut4prYtMKTfbCxW0 z?a-(FucOm}B!bWknuBdD$RtjtwxY(9PM7VzRAZF@AH|iGEwT`<(!|Yq5rV{dldX>J zw0@RGUOw=^j>UNLb;qM9oYA6RwsaTPX~?zuZ1FZY03KJ!#!4$KmAULhBh+x88_FIJ z$mV{F{Y!R5B6EKD+@~4XO6CNjI^|sNyK2bTly;C+=aBcC0CSa8y6$-2il#kdJ zMrJcH0Lc1*0oW9Otp)-8W}z?Is0^(Ucd6DM(=#Yr7;laEerr4cfwjs!oP%zNu;PR3 z$dY`E*&kb_UKFqSvkain^8Y~7&meW5XAd|Xsa6R&HhRj-P8J-&#$)yyug?v290g_Y zgtD$?stbj3kj)Plnke&Z#i9YyTKw_GJ~1&73Jc(OS`LA7nVI7w{?>8{Zf4DOj@^v@ z_p3Ku_;tj^iIF^ktSthPWGc2|HkN7toBxC3`MnYK#udN$xb@Pq!hakYa0Bhp8II)| zxdK{CxnO>vRSARgVOrY{Eov~JMXju>6ThnmM74>@$@x0;z+!r9X>Fx5SC3EnNNg1@ zzMJ3!rVQ<0)9eo=W)T^{zCS3vb@cx&%KZ0pi;wKkjE(NQ!R5=r1L{KV=fTsW$VqMB zQ>kD8cLU9AZFk?1$Sx#JYHJ1jGT>R$+;O35xreD&X5+8%d{TI=zYy9Xd)@0B?IhBdaI)5C8sX>YpQuuI}!b zSLvbu32>)`{{gss{{Y<3a8bypwYJkkp~Y%_UH7NcNAUf8DYB(29*oZttnwiW^;^P@ z8>DpPTMI(qSL&uYZmY=110e;LkI2^!7rS3IO8Pe^7Et-N32%}P$6`WHE`MsKii%D# z0MgN1beZbf3^k*b;I8wXVNDKVW!zzAFufar*PDDX!e#?~g|0jX#22Tyeg= zed3BY@Vxvw&|3oW_P`ZZ6+sXG_K21Md#2ToV+lB=kF0X5hlY~M%uYd>mVnPtCPd`Q z*WswC>-b$(qTj5zXUx|7;l_Jz%$TtqSxgwr7!fU*wC?Hw?(e}CG$lY2SkRgV>Fe)9l*S~}fq>a4(s}kZa z-QE1uut3|$1&Z9-XQRdTlQ}eBHUELNJbJ)#4{cSA`P6!Pu+RZJ3cSQU z-eZ@aWb~XT_bJ5=C-k%y%VwHkmL4PKsa&dYp|rF0_L1z9Fz(V%KPSgf(1NeOz8F9a z|ECsUWW^e@mC&b>N#a4`>hjPW%X1_|;}Lh~ccE6l^2w1=;LlB8u(r+3T@G^lTAiW% z!&F;z$+DKOJxPLH?)P+JgV%_>g_OX7!H*LOM7tPyJaC&0P;UPF961h`Uxp}b8AgM0H7MX1F=h8BL%tHvDD>sJMggS4^`+#|pIT_qC1-l}C}$ z0PT}Qd(j|U6u+8NGpO6gN_8?!PcOk;#dx%V zGS76n0Hyrnm98;~35asjyI08JJhoU(ECV?GA@lf`oDS4yVR^Vl>l-Y@W1iXSpVp;{ zLL3auF8%=s;Dsc#*?S|rL4Bu{x-eo?E8Ftz!A6GMh#z~7F!EZO#=ykjPjY6apv*l+ z<7n~+SBnk!yu|4)#8EIYkKNtTX$%%CkbCHPpa$kw78p~R<|>YGSeVK27^!QY&r*>S z?qnykB!K1Ys7zQ^sCD9CYDjo!C40A&s!;Wrs#trmGrl%(aG0sl_p_gN{r&ayR2Qo? zO!#vb_6CZA*I4+h^Zmj1bKE}CfQQGZ)@qJ$LTsm(3x;(ui0yq8?#|@f%0W%P z>)aWs$S(hm%hi*+1BqS#Cj9hpu*-uk3lGgEpBy5pOMFN9Bc_G|&+)z3@#mU0_xf(H z?9=hegClo;IWN-?G8y|qIFJNP(QK;+jq-P5vH0=p&mD~dJgJ{VNB56jzsrg2))0s> zR)g+L;?pt&qZ#I2aC^d*Y5FYFP)V$g25Xy>E!9$u6(O&ZI2}OCQdAQiCa>5RNj|Qn zAKIn^3bW*)aeu_SAs=O*G?F$uEDqAWm$h5s#u}X7W9ad7*H3XIK&sY${g|Oz#hzS+ z4$m`Z%;LstYKE;hn3c&S^_xq4MzVPGHofj=brVwNy_v)&(1uHD{3DO{%g5{SPu@`! z&|EQo)NO8G->NuI!kBEEm#&ug+g|F{zNTdV5!81fU-2{bG5uC~8a5mjLBg(x<6GR+ zko)!N>4}T$4&6Bf?)v9}km{Q5GlbJ$Z{(-RgyZAmX}!T#`cSEmCm*J%x)Wyu`s&Qy zKgy+S8)54y6<-0L`rce?Q>Xm-z;~8WJ-He;wbZ5HCU>UGU(9P^#FH~$DsH<;AZC&( zVLtF>OUuEW2Wds>BXZ~dreo02GPfoMgC-Hr#`*=?Tj?!L7jrO<7qRsGC z2;f6&`}+I;_&F~Ty{n1!48m@3EzI`$T4}1` z6|H%8U7TZP`kCp_efsFzBqo|xE(otvizVWz0;0iXA5G$pVy0B#WsN>gYQx%f74~hk z_VV6Dp)g1QVT0~Uh{WGU@m>|CP~A;#C1#_a6YhtUocAW88R6H|tV~GPwby#jOc+H8@^nrkg3(WHCOvqLWu^j)I9wV7vf@!}R|mRE89j7g3ImmK;(g|@ z)f`Mz3ygcZ8n>rCB`{4qluY$CFlgvj?Bg!p@K4s_4wv4TuztqvEPHGIGRidcL-!=9 zFloNVGkni!+XGROg`YOpIj_!JxBrf)`R`r0hnmJpLFE1#V}sM*ayDgLS>R5X`d-Fr z&NekgO-8_K8K|4mQz4Q*MW`fq8@SKz2mek0Fgt#fp`6tiG>@57zkp>KTJdpuvPFhZ zoRRH^to3)L{AYnF^7{^Z2P!IbmKU_Fs^gjAgoQ$4h+SVFpWcfV9i zRkolB-m^_E(Lg|`LC6Ycfy_wBb_AG?-I&n%8`dvRj90+!6ihy>roPF2w{11+FzmvM z_Otr45_izN1>z4EtW6jM6y`;c9WM(oPQgiBx-;ln6-&Kg@41ujHewwPiieJjzOrny?t84>@7rb!s{eIxU`l@(7l>V1clntIiwVbiDX1dO{s5qko*jU&C>#TyAPpQ#pv`R=7byeE6PUMi50>8|Tm}d$mMEdmwZC z??q8VW2Tbamg%NX08ZXQI<#!*Z+a>3zA#v@FMkTX!w%gJlR_KKku~2f5pT&rn&+z? zM(2}m*VtNUY_Z;7A3I)$5PrbQ#gWycM~p1+xiyO&PEZkTCx7-`XfneEeTLCeK1Olg zexr>2jWLp3H_$l4abbb)|K;wUPA-!+tR=@USNMBei0truf$rl|?fI1V6$+!?1B~Rw zB*FLuV-muq^kQ{PfDf0YtROwXF}>susEt@En~v zMQ&)=RBv9Viu1n+8*ks9{IC_cZwLw^&+$nmTmPf?+U;rU*IgSaPI&d$?}Js|U#{=N zveXA7n*Z3ZS6X>c+QbAz?O|rXR{f%zT`sI2D*JN;YCmh{lAwFkVqicJ5`*1#=DQztf+=iy`csSLj zv!>cKmw7-E@Or3TzCl zA6$1`-ayV1@kx3G^pqCrjean-H-kDwk}YvKJ$$x=DL7UILAjn9B9i$<&V4yIZzbe6 z7}{cqp-7J<>XX>hS?H=zxsgU7b?i}`{;5>6XJ z>(=S6_H=kB3C5cQ)>y0}8>_w22^yD(fuW&=kc!7GJD_IF+ zqa;lTjytVM2s8D>QH`P-a6B-NtoIy!iJ0R{GA)#$wc15Q7>ZBJ?if$0k7O;vhlr!Z z9y}0XKx049V4ZUf-17^EN68+NYBpr~YluO$I^k_vvLo>gS-aly^*Fa!_|-#n2ojf+ z`~=-Umdygp^Nj^pD=3d$E&qErYs*lwYYIWQu{e7nr?T@g?Z#!6+mbyqh3ad>GNmbR z(aN$iV)2y5r8+=kuNX11x&VvZ7I&woTc_%< z)#1$kXPHaXJxo+&{ItUJf-t>@Ty-H$_<_kBlO2hH=oE#czDR>VXR_J6P4Yfbq z6~3-Wd%tV_TV*Y}%J#|DLiD|*8;3W~)W(?Q@BN@FT0%RWehzQyTJ2YT(6f}Z8Yv7+ zlb*=!i-hQeXSJtwjzkpnu)RG9Rw+|EJSZ^A#0bH8t{!{KhMlLq(?{zI0(xdLrey{9 zff~F%Y{&Iz)^rciV72e|$1^ub*}^$lztirK@9PKpnPU2F#;>t2g%;5tHiqXL4_A5) z|DX20GODg_%Q6W85;O_!4#5Kiw_t%wkl+r%LvVL41PktN!5snwcXw`Zce%L3#V+*W z`(AZbzv@xrb@#s>b$%d6ww}HAnsctRw-axX%|$GtfV-@%YKtAF^TO@3qf+{h%Xs;A z>xKQ+!g_J{GO0X~4AVTQ!!p7j7GE{wC+<_ zcexGg+xa2?MkvxnR)0}xSh{?t6NCNxa5BB^I#uVcO$~n1L>KuT9viDK zp{&kx6H%GZ{9NE6wzh_m)xRpK#?dLFZ1O&A%SD6|Bee-QpsE*32y3s#@v>Rw!)5MTQPKVsMb@ z7<4V0#`h{2^&T)ylXkfK7~gz`FSc9`>8x^lv&Y?>6SE@Ock*^F7I|x!sy_^op4gq~ z4Ef%(vdhV2si_B~Y4@3OmKQ6UvL`wJtC}0bY($F@%It6tr&&$akg4@khT77BQ*OQW zRk~KgPbbOmOQs`cLmCyz)ln7>eS zhNztLm?+^vcDsXU>>$7z0E@WY`%R~@86s7iPs_D`qozM#25#*1iZ|YvqcO{n;@^3# zpx^#-1js^h;KUghNR%4!bl~sAbu}V^|FQdkGDcI=M|MV1(<`a>#Of=l$1fRw_F?I4 z0QO`ZjHD)s`GJBOokOM36T8~!%G^BolPV4r0YW<0s zveYOOe(lZ;5iys&>G$)Q)b#%1%IZKez5+OAi&br^D}nDjJJph&|RG`)yW- zC=apFYA>6HPkFd}SFMKp z$B-e<1WJXDsf!ZL>fK+L(pp&L4@V`s)xlx6TGLhNE47or1y`_nIQqP|z}@{igDTvv zk4teTJOT-8@yhC^^%`0JH$DEZF1wyd=H5xyht48rcO`~y#>ku@8oSPpn;mU8C(Jyg zmYHxo)h1Es8ePqfLEP@2pY5x9imSuRr(?g036B4ey(gYYq7}5xoLmytIGf~O;HZ3Y zksP1f7Ig5&TUZ$E56b9LYIK($LB9zX=sK*ncZE$$Yui}0cd}6U1Fx>gDmdNx3jY2J zoNe;4C4tv(7U&wDuRNpS2X`-1`H$JGmYj|1^hgIbQDZ8(<>yA|Fkv~+@os$h=pb4! z+38SLZ$cL{M*{bDg*41Aj8-EWs%p2*)Y91)mg0-{m)pHz zw_gV0#VOUq{r!FS?^2e1f*mzS^JJrxmlxA<%5A3qu+rN@v$dW7w6lNsvAwNL*BtTr z?Q>vV__vZRCqzO*rr+edjJ|_vRt?mSN8d?f#?!r8jn%-9$w^ma_pJ}jyV^6A`53GkZ8wg*?*0QY^{)R_dN=38(eu05He@#%3!f2Yj zZ#5~2l_&u^wA4!3HcD9Nwq9oXaygVVbiL0Q&jL?c*fduxH^#g<7s#0L=ui8O`IJJm zpZJ3IPAFqE!C|E$b=uw>GP=;1gUQXQ-FI=uuWTP+en)GWo!B` z+t)Cix%5o_8@3Do_TM}teHux+m~!j&w4Vt6w6LzXF1bZ}Rv#(mXr4$h8WM zDDH%NRKT~3KPj4E0~6ro;bP;%_40h}lXvUk7#?-kbX>j4KYMuN3Oz;eSiULK`X3FQ zF`6elEioSZ8EyfYj&!ocvlk0f86pK^CI|}pNR&gM6;9nh)|6R1c3ZV$Y0RGLm?>{y z4qP+`)>`fKcuP}|r&j~J)IjM%PLI2^cv)%1-(q+BRPE_=F%y1@NCB@0^uxxQW=u@b z_l6FL8aNDlUL@myR(#(@z6#i`)V`m2@1#AOT-*?rIkxtCV%Tt*#+b{|<@kWRuH{>J z7uY$^GYo&1t2Jp}&uVbqbKv6?!F!POQ^3eaOZO=SXdN|Va0GWna=Sm%dQk5itIdE0 zg!H^CgM|#?kH?QImB42EUZel=Pn?3Bo#H@$9R*)@r|l``=yUXF-}2yb6G_0o_SyYu zw@bXzY5c+4{Op)Z_^uL>VH3Ghu|S)Iq4ZELP^dofdZ9WM7v-sfxc`^IFFrVnVtfikA;HNg{t9Q?o5GhQyd)bDuW3dvlK;h=vowB60wZnrZzY|IIzVa?wd zjM`WR7EX6#^JHA;{fQZWs+CchbC1Ri8c+vjCTbG7*zzso1>LiiX?JZef&;R;*s1I? z6Nea0fvK$t>^Xb=;J6?p=|6R&KEB5PIwm`(P_y7BO{xgIa!MG>)RXJsM0`=El_1l4#&{X8Pd-8XoIy~Q4kP+j6C(5W-rzI#CQKE7391;{QGsj(mQ#UtoQ}l z!@AW#OWuZk8UZ1}bT3P{@tv@*oKgLXpiz_SlCprB6S z&WD?%&E*gNOBJb3Ohjl8-ABSt97nR$rSTX+ugUHG5xq-he;~;LU|)h?ujztwK|!CL z>WHWpCoFJ`oYr<@FCB-;#q{MwLq?J5lRZ4P>w4o@QnvK)3&zT$?^i6Bq-NtAWq(5G zbR$gV=UZg%cdSp}gQE!|U|HIS8w@C90@!po#(?3AqBd_Cf17@k0bQ0S>SxsJrfcnJ zxnOW!4-Xm0H82KY7m7ibOiu^khAw$x#${#k4{ZXze#}g+m-;;tdTmO~gUAc;ePmpZ zOR7cbxP^!7D#uYY-K)yCo&l3Qg=FEeIZ`-}i2q#*cIvKV^qG`Y-oHX+wzz4;k}M!Ir^!vhe8Gc3?uDVO>Se(^Uvzb8i@HRza7> z4Wlr<%ecGhS59|3;V)V~VVItVWOVSOE98uR&KoK2M~yK#IoYU?31X06yx~4>_Z0l< zpt@FgOsrDpHt-B3)IGVse4+Dpc0jTC{`%b0mcg`krz_p4CZk*$Oe-fa<@Pt^L6&PU z>?H?ksHhpjF!!D9h&lIy5zA$Lyiaki3Fe-u5Y`4*P+ zzJBfDt6ld=NnAqZ@Qt0ua!a8~JhenB;GQA}P#9AzYjB#`8_UjV6Vy@D!TBwsvD;DN ztfjOUGRG5qjal{H#uZ0V=OzHRn;C1*i-vyk5ZaAtMk`A@@#>-TT76MDNtB$Ed(Bpw z@aSI9UIY$pWmD|v_*2t26X1qRj^+uxvtAN0+(Yfn8*s@}PWanTlpBloebW!$7Y9ru zw%L?`MB|uJf4T|(!e1bR1Eh$;&O{vHkz26%Q`t=|ic-{naQx@7?ccs^@?StgZ%0QW zIrsq1vbyw`ov{EgC3MJ%Kf33NGUEP-ps^?igCaU*YEUoz`mkIaa6Xona(Cc6;IjiT zA=VIZumART=#~YtNI`jqrbboaB0TA-bX0;o5i33(Nkx2e5c;Q-zb#Y zOBXCmHsQaEwhkb`rKYE{`wL0^1(rHik6#Lh_n)3jp!>g|aShdtjciDhmN-F`B%Vn4 zT!6@0B6n0ootyiwAqXlG__0NZ;e+<4nUMfJu8P|~<-Tdc97jp>JF}I9o5j^@MWgmt z*ULg{oK#axJ9>lqQgkaFjmsN(Zr4xW5)<)Vbsyulq+BpqGhcm&;0-%mEQ>?HhM%TPp8H=yj5HgA>r1@lFx;1Q$*46{; z%zg|e;J%XLV|xi;j&GAiopX$qvfg*CRc@0ET!+>=5H9a!Rp7!Fyp)&A0^#)dHlkSl zVvu593UQTW@9Hj1&DC*=vgKyCm1$YmPdbi4v07g#nbT6VnLR&pv#tgok`VDecMe_8 zzWaPqVEq1f*Cq2%Yd)1N7K;5Dv+s`b7p%eB4Qf6b;q2*V>FM#tcg9!GE|)a85rGwO zM24f;y7Q+(4$M<*_9hqWPwp&B z63+zGYQ2(^sC2hx`PX?Qiej z+7H-TaiSuAn$FjNj~D7vLVUJ_p&YhX`PgJF-cj(~Xiq(K;im3wwbr#E)#*RknoCBt zO&1AA>W!Sjrtyp)57$d}{c=7GN5?7y&_^r~%fHs6H&$WV7H*hQuw0H{yJ+>(wL`B2 zZo*}F#2d#KX3(EukYG}K*54_(bM3(P5q!?>yA~W}`CvY6Grr82Xau4U18Sq$nx|U#_uU^SlHVnkw={>_hc}jCMLy&Fq^Jnk zEsUD9?g&^O3>?h9zlc}Y;x$T*d<3sMJWAV;X(u3ZS8eLh5o^sEN{g^t6cKq!F7Dt^ z)&&(lxt{;@bBSlSm-hNCL^JUWdEq0}y}=f?ZatnaXBzx`pAH-4sm2e!fm;2$08E0V zW&}f_r-}VPYnuCSF3+^Jvb}vo#=QPUgkllVt0jE1(G`Qgow?ibS?MrttA>yO+rWB ze~SgED_j*VvtACguon9CP;`xk7X75p&WYnD6%ILDOqr@QQ8IY(;-PQA4@G++9|>Hh z(U}osAGfYnMlHEXdd_Q&Y7V~f1;;OS4#cILSE_3NEnCmBs&5Y^6lLmYS zD7SeQVsp-O%55%`GJ+7%Jm5|S9)Y+PkJZUymbGM|Z=RCz7(NnzsY}Kbn4a~?dqr(y zfsBe5H^xuyYM~l_ZueCSlib`JL2PGF@TY5Dtv`_t-yEFeR-o~BcP}`;RB5{HsFDGL zSuaH8-RS8!1Re<)+Hhk(Kkr zhiGwI1>)z+EyThY(6{X--_bH@Lhjwa`;BBN4rU$0#q&Jx@Q^QwP4>9YG=4puY5ML6 z-Zx(!4ktmTeRscbVHEDuleL_Uf#nb!x+3vqiRaovmv@LE)EG%9M zlzAB9sWdJPcy_Yn-jwZ}7cB%d3eVmhm1ro6|0rNYb%!0wdep?|d1*h<{!jr-11_h7 zL#f;uFyUp<{$$(V`=cS@oFS2R&=xP_19tCf8N!LEgKNSumO}%pf{NhT^`q6we7axT z7uNz)W%vpkxhK^TV|$ZwpX&Fod zomkeJO^L5EKVu`8G5O8@{>mh1bst7VyZq_QASH>jgrnAadnA;A>Z>ZX4`#;)QT(hy zyKBSo8CzrXvR$qK=@7}!+>odnD$s-6sImL$KA7mwLo-3s`FSUxY~p7zj-E7_*JXVo zS}Vfl-xv|Ta5=x&K@%`t=KOIvLH{e9-Sg1khFP$_Li3onO37ev>bz;vRu~+XRourP@@XL59!4d|HXu!800+35{uXR=iD=pN0?TuUi@Qu{K|Q8O0~0 z8(D7oX(=YR=zx@fmM%JtSx$ZF_bonWGNPdJ@gaplc@C1!U+N z(heIzh-ZZl6R1rWQ(~e0dLbsZvURuBb0yoiv`1blQE&2}8s&bj=Tp{POvCWBV>_!i z=Viuzmh}UrJwA86?lenFW7>(3gzj@WX8zp?g7BT~ zN^at5+{z@Y`ZZn`F3&Zo)L7;thQ^nR%yCoXD@E9-^eB*GE+w zny<9*pg=eK5~;W7UH5=E0?pTj?GK-y{9u?#97#y;Pn1AZT!*rxHI4&p}$mGtbiP;SO0}d8v}h* zX_2L*q;~WeLh_8#wm_m!Q%@x@)Rog4hEp=G79;QPl5iM(;?zDL`2R#C!+NEt$~l*PGuvUM_3R*ZdlN!{GCD0`KN`Q`fUq z(k@zVHTUI5<~`Vsy9F{Q1VTE6^7!Lda=6@UBK44c9r-850CBOG`NEi9Jz~l-nBAk@ z&TIaK_?T46Gl{n<#09tPtulunE6&R#74zO?>QgY9#+xI`7`Ah_j%81nfpkMC)ec2J zc-iHxpz{aUwgm7q$e3(+uGeLqd?xIo#;}>I`i^+S9-mhp3hmG80}}sXr29a1y0-q+~+QhPvyZe`PVT+E=_ow(eSv8%;H_7At9~)>B5C#i$t4HDWuiD}SxS;)Xn3iDTkxjrLRTQ&Yx-k8FN? z-bvQh#5nXk)$Q3ll9?@dmCtP{I_s@p%8S}Pn)wk;vi=v^vR7Mi`MuqYKtrlA;dqTS z#hMji?{S2dz5Sk3NprSv=?HIHQ>LYb`n6s>&Th|xPNIh>UwSePec>al?pF#CGPK$} zTY`;Mj1yXIFGW__S({!ruGfPV#6Awyg-X-U-M*3_L^s}TN^W{*hD#<-Iv87UI#CKn zAX}LB{b+jd>4|=D7y8<6Mn1dgoS{G!3)$rQ?&qV1q=o5R9OE+@?$h_;Ee!A^o)dNR z0ogO;4nH!U`gG7(tLWd->qI(#APfpmh|o(LNHDKb%yUyIasUU@=x8c6s}I}<$o;&t zMN8*OdxjYbot?K@{oPPX@VTDN7$R_YcALtW_wFFraAqs^A$vZyGi6WUgxRtf`@7Qa zDfq}kb|Dte<)9N3>auTMgID+=E?Ktj=O>Ic-T}ND+X*HRNWw~;yvyvu+PGtW($#f< z7*`G)yQ5&tQ|<&_!XHh#F{S4Bg8IRdYWES*h;5%DMAXUP8Zi9Mc_@fKm-k1Zrni%L zt&;;NvZ^_@)7h*f^S3%3%?P>$URbRG)_etp>QFq))-ml&;S3-cr0^`x8e+LVZ$Dpw z<7L#%$H7Z%a>~X;7|bHFu<88D*_9nNSK<+80UaYETo!-3f3{PUZB6kEeH*SuNMGwhQ(tlt&^7FKKqsIpRQd7U7 z+y}!)_f6K5-_Ive(<45zBrS>)R-YM~i>25yJ2uTe^E4Mn#%^8Sbg@_`L{3^Mv^sU9GPM(1F^QP39wB4t{E>>g#!zFILHpv{1A3}lVW|k zMHGYYZpi&@>oyp4rxKilWLY1LAk&V)NH%WYQP7z63Sjy@T@|x@#s+QDgXMPWm&}|4=vy=$$76*CS*A`A!yT6Ea^;`;U!d!yxbPz*=VcQ zslB6-MlBHv*qK}L44n)vQ{<*!>q;LnZ%J61QbLNhq((gtHBL~R(c#hT&O!r@$RuwO z{C<@3}m89rC2wyhJl!Mfw+H6JPXL zUq!Ai^1s`{@=ikgHlI>rEexsVM9CR=Y7dkzR>8QKBl2W@o_NYjHO=DHqHkc2nlbc# zDmoR_sQFHzXL_l+mD7CZ+lTjyJ)!wymtr+hCU*962LWw#cQvm3UjTM7_EnVrs+3pC znEf>^(XTVr16#mbU{hwgE;+;2d6-;lSpfYtaR49l}&B#m%KJS0@%Im2-#2Wt2$!*Qm&cuI zsNTSqsw}Y>w+T6Y*b4Tz(aq}$cQ6_78luxgQ@GW|X~xWAWcXLQJv9gdgA%0)R)`+y z;DK6A=EpcBhx%{$Sh%I84}v11rV53|3Rhl~<3jf#2<{kF7b(|FGfymZ+Z%*0tB)Qv zUaCV_nuf5=um2N`7he6KNd2#Ayq3W_7X(MG4_<4A7#yVa!6D@jzE8^qMe{kv^PYnS z&;awfzt{--5ixwGx3=nPqwwGq3$eS}ql2nVu^^}q=7^BgA1n&jd+)8*vuEmGfnB_7_ZB%0A8y#L8SImT8i1?kKc2!*trle39h*garge8Aj~xl2tL2gGa#j!@ zZIU6rqKn{>(Cu^KxVre>6g+uC3Z$JuJNms(h)L*u|EU;Mf@M}AzcJKA=c-wFjJVU z4MvseEm_I^1}_q1N~%;RSC)iTKzCvkO(tmD^efc zZw4RqP;p$oT0T_dJ0I+?%X>mm!;;e1=7pd>btAD<*NRGml>pJ)(B3^<{$ikqH6y%0 z_XydFX2GB+YVn!{fBN}-+K(9E2s8N6%A~}!QdOauNVJ?PK(&6+N_;p4K7X8y!kbf- z%Ed6vHtMcvGumQV{7^myI$Fn)C=>Aac_vP_i6k>GR}1oRM^}YB)*^f$K6)*;j#BB(|V&k;{@V?fjp9PQjH0*E8~t%hL8o(qo80 z1G824hNSgSFI82t_K?K^;>*B(olHFWCQ?Xxjc3x``0|F?Jv9gEkwm>wD{3b)W+zA@ zsTmlmeaT1fgQ2(3YuNb5(*d=U{_#>d>sfY%>a!Jd-MhvQh=jJfyhlF(Ux*^B zqhqx*(&?nzT>cotvA*@pRXkAXb4>Y=Qdzh-`CkJQ{%06U)K;(59t7fe$4S?d9Ip#V)dJn7hc{YSO5dii))$je zoT}_Vx`KK&m8_D5U(w#R^5-=G!40YM#DjF2_M0IZX}fr&n&Fu(b`8)5C~wZaPDtrdI?ADk;YGIa~wnwKIi`ePf%0 z(Cc|8K<TInKdS63nCo@yQX`n-l@(bCmIbpq@v;Fq%4-_}}9lfiRzxC7MT<|8?iz zk%^yyQSFb4mp*Y5g3BzODNRieH`;(yhH5ubO8~?eDqx8D3olTy-$shY0ngD_xxp5l z#~b~y*GrXv*fVNGz!wLSLjm+gAhZ7&?+AF@|G~Nchk*GHSOMsY|NY>#L#o08}J^3gJg+x{c2;Q#gQugr6PD#>ox z6MD|_XmSQ7?&I_`ZP$DLrZQr8;EbK3_jlL-2-_I9ANe~x`~N-LZaI^o#3N<6zC6ZK>h{~Pm+gN?m&m$7g*Dg&>lJ>d&CY@hw#P=Zj#>GK=YE6ku>3T z+MeAtR+XZsa3Lu!v0Kji>H{7qnmqxc`mhVy#5$kdogk};qqWDq!vum`a;Q+(SJzHn z`j43io4t9Q2y&=i2C^~TefpaZ+F?NRJ8?O=${Eh}f8SX7<;$N@zp-7nWNOfj8t6r! zpsIkJnd8L9U(`=-@h|;VJ;vbxifghDKs$@1RvjPV#4RD_H259dHCSOaa?F9-LN#lF zuLVw|1u9ZiBg#pOKQ3*P0^3y8+{*S#YUUP$+vEJ7bFqRmrcGJ5igQ0*U7ZQ@WUG1> zWhnW(S+eG|Cymmv(K4pzMpSP!rL=CHswJEm%_~KKvG#PcB@0QLYFa7qOjC&@o0Fk$ zob`dtT$n)V*b&$;ta;;OT$foRW>6D zyY0z%Mlehlv}*EHW_O5iFYkKRf~e|j|A1#ke_>h@>9)1qxLms8%kgG*z3Qenoz4XG7dhjf%}u9{u6=7z>9@B?+4aCHcc7Pyw-J~J zf?gAj1SDGHVP`|KN=aU7SSY#MOff4BN@FjZk%8tbB&Q%2y4JIio9Q@BTE}N|c^-mN zldrV?txIRg+sVRoJ%CK(=Ct%q`zywzT!fs!9}K!fZbZIU?PIpz?FEYImOnH;75-zk z@a&_uV%m2Nj3&4BFg13wnd-O_Z?>r-=EH4r z!Q|GP^=aTF9NQ_mtjlv(`Zi(psayo} zw5Jwq*SGr%$}N=QAzZ678FZskH2T|6FF@#mi?N1u-reRLPQ1LlT5uEsnBd=Xhd+7K zt@D;U2U}AilL_49%WS3ge(=LPfB%?6W1H5HuO7O@czT{h5hGW@vL}H|wNU4lWZrQ8 z@?DvfvjL@MFH|>Oj_*)BYo4$5ewM{a9A5a`mlhuvZw~5q;{#px=4HExa50=*piY%r zw-Mg<^p0-ImEgXt++us&oW{EI<(p`m=Gi#R^NB4-agy9T3yqRvvwZHGyBFHuY8R(uya~Ml7=)Q7zBg6c8 z;J9b@3^`JJWR&Xp+j}91{TJcelB%U@m=L(c`KWs8+8@Cdaw5I;R%nJVfdvs>*w>O) zkT4m&+*<~Xk~KVLTz?JiEldTWs+=3GdI=;y$+54Q(SFW>htb7my(wZhgm`Pma!m54 z$`*k*c(I;KI;^&L%qzt5$}#^U|0T`f$brB8!5kpcq%>HiUsaP0P9brCv`^6vQa>$` zSc?Wac}*~BMqKkWF3qB52aH2{Go@VVz6W$4Qxh~bLSH3(s%bn9Np%j@8H9TTI=S!UEk;a;&ul+xYdEJq zJY`7lj9xD{ZCz0}=w`TgrfN<2PbRNpl_GDH>M@@yK>{wbK7>0*hY7+)w6M*rTEd{o zSB)#g-0<`k1Zo`;I}As>EswIlrv@o)9d{==Yw*;g!g4(a6DEwl8A87ENz*=&SW};| zUYL2?j7h{-;D3Y}WIP#(JEJdu;HK-seV!W>!N~bG{UzQagR0|{GcgFUR)J-LU0Pe$ zlr_29Z{fV^?km2F#y4jtq~3Aw-$CMCTHn4x?-Digr9eoec7tS9rWe0TQW3P)W2L)- zCoJ__C(nTPpoBG@F1~ljx0SM2uMM=0fS!KQq9a=F$4Q`EMi=7GDwCwF+YF)iM3aWX`p84lCWMOU(NmpT30&sP376g(F4Wjv@ zkKx;af?SsPF6l{`UGqfA63PVS_ooPa@o)th9ZU~Faz;4Q?1zeWc{NrW77_uZ>lOWj zGZyr(W3OI-0}TfQSBy&+a7-7zJtGjd%QPa^njy?DD)asE=?wPi}NW=0&=n&pB$RR7yUWE14eRUF-CC38aQ5tU3kW) zR=%O}LxoQWgY)<~dnHT!w}i=;BLSrx7tCbk`bY9tFTm=gz3zc5IOedVF2|A~becT( zSC><}6=(V5KU9y~`AhgC4F$czOnwvWBkfHc_xGDm1>UOjBiHm?qnG&xkO%$v@gmll zlNup^WwdMLK*VY-L?)v;VB;+Q=7a0TCJqU^XIxWnk(pYdPo^EWaZFu`|G!+5@gsA5qS5E2nam zZ6W6us?~Q}&yggQEewzDc{N!~!tAkYbG^^zRI1Shdep)*(h1B1NqBEmN|et+fTwA)yV5?>02C;rqhPkQB5HM=z8kpA17z#aLlBT_%riz`5g zxw*jbCduAxIa1O39hmUX^8)$SopSbf^Z|+boPBqlU*@=qIxk9;sE_UM~mIJ|!s%I7Da3i%BQWT)dx4lP{YCfJa$ z9EYUJ%WtVSi^%Lu*DXLx*p)|9{hx)moTuYT8o(lE>ed-C4z)lXcE{F4(t`yO`#`|_ zmowe1FtU+IX!6PC*jvKnwvk(kElZDx_>lEk0Wgq6$M@^FyhN>1bPaf&*{i;LqJ}$g8?Hv))g_|6el^YAH zT2oh+b?=Esfw0J-VB&Qp@<<8&h?R=$aV zi%MkT-nY-@aV7`|3UVkZDwbgCwFc$nP~Ti0gv?sBIO;Q=j*zV#S8w^bBsi~5G)nf| zR6LL1A27eYe1oMtSz1W!eJOOTh^NU+Lyvf zS?PX(r`p&(h2Fm19)c}{k30u?jEkD1XWgG;DNcqJD9=(KNk4fIezT@EX$d6qx==m2 z)#dguOkYYd`GkYs-1K#$fZxl#o;Rw4H^0($KQ2G!&*pbB7KekezsQlomI?=7od{|>b`4z|xPRP7h-XZ@h6 zI;%EVc&U*u_9S~bHKWzC({Fb-XHHKG$fBapVxx6{^5Xo!@qUmHDyO8;YpXV-@#-=6 z-N6r?kydKPi;88(M(vZ6I90Qu{YDJa!BUFNKtc4mQU*E2WyQtC4}~lV8)FhodzOsW z8g7EQY=2czu|i~0dJb{Oin9>v>gpn+qeZ`bA*YRzWvm_$?6csCiH`mr67orfTLO5V zLBU3h8<(1vBiH)vB?j%g3B4xz7?JeZFCuH)em8Y09z0qoaZqR6C*i=(iNJA@k;-Je zwYwV}5`ubvf3LEL{n(&J4_6!HfgDxWHxrmDi?NNOdp(B&E9*U7y6whg>)|(P{@&i+ z11}z(5=!UHeosvmN31TC(jofC6O_30e^vVR>yTI`pfQ0%LK+t+o49xvB@OCdw-`i4 zBLW!#jV9Iq{jXcpbBsj(x`#=z^3gv1i^=$wBKm&+KB8@QkgTVjs97WU|6>W?iUG(gYU`(uStc>U#4D5}JtQ|~k9FJh3{4g-@VZ?<6 zm0VNz7hFCoZxA$}Fyj7dy26pe8=N;YKJ0H|t1*g?D=?CoZy?N7uTvP4qJL06oUz?h zu??jY#QT{^g>ZHM___4GW>f3H))yQ5Be$VDeiN-hhb4!@I(Jl5RGXW-J0BZE!}SPs zAp}xC)UT3~p-8Bx-m;~FzHm{|?cgTNcN;G*=NvGonrlHl7q@SVQ z4`9{a^YW(p`Mt7UY`jU$#u3_nDQ@UT<(HO5L=zW+jLVQog~ZR#UuiZ5r>mqk~V8XEwXo=}&2CWJU&wnVDICDwm>Ym7gmj zm_y;W#p)FS)6;763k!%yNFP3ZLdV3!WM*dG9?M5F>WlB_>IufA{KUeN z^Cb{<{Es}WmzURII$vMF8-lMA60PIov51%ywpWMR0ca$shT)@*hx19jakM#9NG&Zb zmeXZF#>bUl!otJNmLPmp<`b>m-9ZE#)}K5sEv3W2zVLi{xPtLK>A=;hv(Ig2Cn6$B zVm3muv$G2$;6PPWRNNd$$q()s9{#js=LrFjE;AZlTHFwi%Ap<3yZ_~ zx;7G@`{W77u%9{%zx}%SCy(`7ME(;i>u=|SSwmxE``dFfDJiM1U%!5q zm5prig;y-m;>ym>?(Xi+G#*HciQZyz;F_tm>k1!J!)^O}X3nbUq90fxh7kV92wX)W@C9-Dup-u z6+=|I5MFFrmG#q&KCW3GB>`q;g|Wfua)Sb$CT>wt(b~GYm7^nBEiFQ@w?C4Rk*S7J zQ&SgdR7sq1zJH(D%pSViW7Jn7ox-m4M^5m}^$jz)5|Y#O)7*RKd6+3R)0{^*hajpOF_Haa=E zH=KZj)BB#&<8q(63=$sx21Z_9KG}lf!-o&gHx4#Z9Y&Ja1s^YOB8QDmgjscUH77Us zOLSu5bQ26kIh32Kn?FQ+d`%y!5^z;jrwD_?%FFE_P8rTbo48L0b2XBVkoI{#8W8#Dft;Jf^di)L@e^f&ECQ3rQUw9_BT`7BY+y77hA<1P%>5dv*5($;8F^ zxp1yR78R0?jt-1ldb-=W7MQ3))zU$`6?0EdUU1w$PyUe8)y)74IB+hOanHoYCL${< zdnN-ZV1@%NrAoly|G4K&kELZzBWp`!FlAjIHpFm zrFf}MQzEaMeR?^P{4Dlmjoxsk;7~3Jh!n%o!^3hyU%s52oRCsc1;L{cFK=v!SX$D7 z(^!WZlEA1-!OV<-hKA;Txi2myg+f6=q1EIbCz-%tw=+tfos(ndm;N|1Hxj!3XBSXm*(cAk7 z^;;Jiy80?O_ouA7FM&}uUnVUmIJl+1ALTQ@Ke_PR?ldp|VGI%Z>&+A!PRDKZ&CSi} znHjs&b@9!ij3HlmG}b9CUe{xoZf;YriwRXiPFolj78V-SlCYedoK5W}jbh`+`x`8@C?cYBZjJ!NMzEI1R+!&fxX?GfR~7mq zUvOTaQz0WBs9;Vg;Qk7W4HDfC#_KB4A!GO9BCV{}Y z<=SX|flncmob5KBzXStGWU2XTHBq3Nfk`R5$-_raf3Wk@6V9MBtYf+zD-QBPWs-fo zP&eZ>GH!HK)JeaU%g(GnKs9`URQ~8fLD4xE!khCqXO26QzV_ZPl#bDU8||^D^7uR^ z&86|!@E@T){%Phun5|x^y1mG#E0xi1D7T-89GaJz9QYcdv%0EpHdfD8&Y{og`b_|K ziKu&jMt8X0iGZ1fIb=cSaYVmZj=Mt5@-lVky6cx5nN6+!rvS!7=v_V>M4a z;LMkATrEvkp5VG)?FJX_RV?AL(en}I{({xZk05fbI;`*^|Lm@D%TjYv2>?WSO-;W3 zxAzZs7e-fuY4;abOGP>rqKYORy~@|GUwyN(5W7Pqs&hM|fZ^ZhJ$YCqPj-hsoxl&4 zSpS(Y=XTr|9=<+FHY4!9@ke@me3z0ezc>^L($C8|h|?a9)m&BA=qwy9fhL8tH!lVS z*u9NZ8q$Ueii*KOL75&k4A(cxnc(YfT#h2D;;> zf4UgSk`z+}7Ms-0g*iD@FRDvueAsC(~n515euP zu{sME2JQOaN%2WZJ&iZI^5qh2yuG(_O)5lr(ivI)RR3q1q@<y3l?NI@~0YvHmxUN8Vz;K4gz{JiLODgoE4?-@-?@43% zOnRe+GHKrPQ+}Ct$&hquJG8U9jH_c?N(Ocuxoh_pA@_@2@lc!rkks_nw=&Az&Wt~2 zdEGhh&)3DApR<0}JKG#^G+km)y<76SBI5IaNSEsHogCZdTc^$XS6dmGi91TM+MU<| zEU|oWpxkfd@o3in_yGS7Om+YoQR|{M4GzGMyYe3J+6_flLs!->ek8|x9FM?TMA zHH(%lJ&O4m9)#em>kt-aXx!RupgA6lCY1|f{!wpW=<~iN?V=`i;oz8`pEn(=$NnOK z*NP@!NX`s>c!XzXXAdL3;R0CR@n|xxMg!6lSxAzhApalw)iX5olTbd+rhC$ReK1$G zad5a`=VaG1*2bcmcDm8Kvbry|r(z>tWsV&|$d#RB{Ow!jPT_A6Vd2%~ZJ5tej;ww0 z^j+iqFBMskHL9%p9`3HhWn?zNVP|4zH=ZidCggJt)2zWTo3FJixh(P5$w@7>DcM3c z_vro=xwF{BGmrum_l0}I<$PZ|nb7d5P^C0au`2@1B{3tTEJXy`%WqpH+Awt5kFaE- zo)W%OI&B>@cw&_K(I~12r0ouw7s8zYvaYlR*zb3f=1HZupW{`i)myryJ3ZXrBOs%} zzh`C+`DfHL+~-WIY-3~dfh;GTCgL~Jwow94 z_c83n8on=y(}_JfT|X7LD6o=0Jo3@e(~YR^oH<7l-&bCmOg^5oXmh)C{t1&Wan{z- zuJyc;D1jiGH-zcoQOKr#Rohn9aofP2t+skP@4HGB78YGS&{jE9)72#bfbGMFoLZM7 zVt}r$;}~!G-}|9wpL^($Kqtq>a-0flOy~06G+gZuRLB?_eq>-^fC0t9TO6EpgY(VV zDhpRvL+u=WOT+WySn-is&IWWMBJK5CnmCEE8c?&D?F_B`Tv{dB0H~(UZVk?4FkNrw zUoNZFWEk8Vog9-P;_pIF5HIy=Pc8y>%l9-iA(9Djg2te}BZCzT9ESM?^!TWMT28QKpGXO_ee>?o^~- zJ6l~j9IUd?OYTZaN)i?pUfx-W2o6S6iI3M}?EK$2$IL%C94+!ebgtikdn0FNj)p$n z1VSGl2?U^BMe z3B%97n9A#v3<8kD&8d;~d}DB~|LZ9EsQG#)T2(r$xk}@Q0Nv>d7ZQNd_XS+by8Bkv z)&>q2Jc2?(R@;MP06bjAo01v<=d#RckA0q(*R5FaOFW%=XV4J8K_3Y;r!?b2B-U!S z(zr$SEQ#FsZfP)N~GZ? zI3ieYe7l@b!0zW@n_5>58MoLigscEXqX9>B*Gipbx za=497O>K=AV2=OMY3e=$P+^$3n1b2gjoEEDFE$nn1H)R+z@lw|$K~3*MMz)Yy3AZ; zacYlOmJ1^(zY%fXCwg&=oLQE3v-1s>NXvYGY6^$Vr~R?Fm1YYef=6%Y>`ZMXOKhTLB0Y=o+_=$fKLx5gGpr#yDGiY!H^rh`Ro1QPWW_njJ(HaGh6>Al`5o#q;Y5&ZIN;36G)7_VzN=) z-7U4(hF;iG376E`!HOUYpaRIt%skGx?jG=jA>!uBNT|)N1N~fIDQi}p3xJtI)^sp9 zl5luyzsi}i&r>CP)MV?nt9W^3idUhK9 z-Mc?k`~dw5*?vU;<6|{dJ3n%J{z~CR8N$1SU%wobivFY-iee;Dek7+bCTBWjLjmu$ zvlQy&;5^;uHZjh|32!2TVw~!KgyE31mR9QIY`ic9nnd*zGFYEgb*!h*_n}`fl6a|^ zX`YEfUU=CP|H+OIB+$Xo>}^N!CXa;a>1m9}wH}PO#4_6xg&paPYb~(ZAILK5p1_VV z_rkLw|9JeKuSu!>)G#Yi_KLCdiB2B=Mw_JB6zRH(Pn z?aXYl(SP#<)Vm;vsPyO@!bp6Hq(Tyl1+xB>vw6WJc=9g{Z za5WD7ZM;sCJ8oeqWgp-n|7cz*kB*I1)zAA1-2XBg%g0sQ0+gAOPcAnpDJi3+M?2-m z<-A^=&Pfs}$<9JtI1&M0Y7!}Nd^nVp5JaiXgvy`BDzcAHd|5HBD3k zO&++$Glr|fg+8{So3kxDdwb*CFg&hwD8K=_gGpZiXHZ^VMF#5NBkze_&#Qy`UVx^U znAv;eCyR9>v#GS8-f4r{_1}(`gTNgf%+^rS(EJ@{zq{D$y4W^lzdw~+yerAZ;u+kM zN1BBLx0R!EFsr7aK2|HP2$&p)ea#Fn_LuNtl=Jg^i7YPGQF8;J09!R(%7O(&o>Gxg z5+H`K>mn&70oC+I$EG_!!zDJPNHOgD0wc&{wNnxEV!Q=?w;cf&0i+UlbzVQ{I{x0qH?Yff@(l` zo*&HNiu6d(jFO~ByKWMRQfm|es!I=WGJwamj39bQ=veZReVE$)-)ctM(}}cSfHIn3)ZS-`&Y67QMEJ>Ya`R_4W0$ym-~)88k)H>PziU zRy#bG`aLU5--003zkb9sCD zb2$D4o`5wSGBkZCwqUFdA0O$nh6a9)Xk5W zegdXwYd&{?;J5qJo76KThq}AJjjk8e)_dq#8%d@(6CqhP$VupIU7ZSC)mSfZ|5Yvh zk&}aI(=-uswl!ptM@~+`{H>XXlam`18($~>PE23#iOON&FsOZ9qmZ^w{ZgcOr7R^i zV7i$7nhJ?&WaQlS&MGxc8=*)oc%o3%V%LP%wHZv#$vI7Ecrg(Xx8rzah51AzCl}Y+ z@^<%f3yD1=8=LC<)?gah`}e;9=3m)c`_eW=F#Oj@QVy%ya;BBGRHs`k)^oCbe!08k#I49I9vJ@AmeW2^%ZDuSHh+cg7MElcP?S1+(@LUkm;8 zJ-IyT=$#GVSh5rsc6Dv>GNRRFx5=_fnS6);Zasu-i+#}j@i2|Y&)B}MZR z8kRq{!2RcnJn5bBax!o1UKaOpD!}naaI+|~43%ZIZO<2AR#{B+zP);$k<(MdgXD=u z6(~RIut(%Se>#iot={bDDGY#iUI7crlFy4Y#ve`^|n(~ z^w0VEXP|3*%9Jitadmh7rf(>0Y)rAy7dLjUJ?uS~rOg9`2^lFzN8&xIEeqSD#U@Uk zhV|(!X{iBDhg()EB!P!>%ojhf%PaqN4hej}uH_u4PW@!}p~U)-v!RyRxPRKSRGY?> zz;dZNyus!0lhNo$-uZ}?w7j*K$p33IMRuDxMQ4TpVVq}q_%jZlzH*u{$VR)J|uE2 za>@HkvC3G5s*RF6jSQhc=YA3k-u;SLs*mff!Df+q(SsX(p-Rc}{*IzlGN<5}bq>ek z^7M$$OuZX&gY(g5!x3}{+oLbu#{PU;p8vs8xI~dOD=X_=Jat=CTi{HS1!Z1dUZv$k z2w+@6T4XUEG<*oSDAGEC<*BwI1E}9+$%Apps3&A^g#V#-FlPx_Q`Y+MaDN&SzSb3? zZ))02*%p93uUc)tzT{J&Ua`5hC1>A$V#o_VmeqajO#muUCy9?G1(1lQAjhVU=jV

~Kp?CIY#b1N0hyZNNEY9<4#zVGx$HzU{$%n>x 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

oSGY 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 b Date: Tue, 10 Aug 2021 16:36:09 +0200 Subject: [PATCH 045/110] Add OpenSuse 15.3 build and change registry to ghcr.io --- .github/workflows/ci-build.yml | 11 ++++++----- docker/Dockerfile.opensuse-15.3 | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 docker/Dockerfile.opensuse-15.3 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index b6c7f1f4..4cfeb7fd 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -28,13 +28,14 @@ jobs: - opensuse-15.0 - opensuse-15.1 - opensuse-15.2 + - opensuse-15.3 - centos-8 os: - ubuntu-latest runs-on: ${{ matrix.os }} env: - DOCKER_IMG: docker.pkg.github.com/jahnf/projecteur/projecteur + DOCKER_IMG: ghcr.io/jahnf/projecteur/projecteur DOCKER_TAG: ${{ matrix.docker_tag }} MAKEFLAGS: -j2 CLOUDSMITH_USER: jahnf @@ -57,14 +58,14 @@ jobs: echo "BRANCH=${BRANCH}" >> $GITHUB_ENV - name: Login to github docker registry - run: echo ${DOCKER_TOKEN} | docker login docker.pkg.github.com -u ${{ secrets.DOCKER_USER }} --password-stdin + run: echo ${DOCKER_TOKEN} | docker login ghcr.io -u ${{ secrets.DOCKER_USER }} --password-stdin env: DOCKER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Pull ${{ matrix.docker_tag }} docker image run: | docker pull ${DOCKER_IMG}:${{ matrix.docker_tag }} - docker logout docker.pkg.github.com + docker logout ghcr.io - name: docker create build container run: | docker run --name build --env MAKEFLAGS=${MAKEFLAGS} \ @@ -189,8 +190,8 @@ jobs: declare -A distromap=( ["debian-stretch"]="debian/stretch" ["debian-buster"]="debian/buster" \ ["debian-bullseye"]="debian/bullseye" ["ubuntu-18.04"]="ubuntu/bionic" \ ["ubuntu-20.04"]="ubuntu/focal" ["ubuntu-21.04"]="ubuntu/hirsute" \ - ["opensuse-15.1"]="opensuse/15.1" \ - ["opensuse-15.2"]="opensuse/15.2" ["centos-8"]="el/8" \ + ["opensuse-15.1"]="opensuse/15.1" ["opensuse-15.2"]="opensuse/15.2" \ + ["opensuse-15.3"]="opensuse/15.3" ["centos-8"]="el/8" \ ["fedora-30"]="fedora/30" ["fedora-31"]="fedora/31" \ ["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" ) export DISTRO=${distromap[${{ matrix.docker_tag }}]} diff --git a/docker/Dockerfile.opensuse-15.3 b/docker/Dockerfile.opensuse-15.3 new file mode 100644 index 00000000..5eb6427f --- /dev/null +++ b/docker/Dockerfile.opensuse-15.3 @@ -0,0 +1,20 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM opensuse/leap:15.3 + +RUN zypper --non-interactive in --no-recommends \ + pkg-config \ + udev \ + gcc-c++ \ + tar \ + make \ + cmake \ + git \ + wget \ + libqt5-qtdeclarative-devel \ + rpmbuild \ + libqt5-linguist \ + libqt5-qtx11extras-devel \ + libusb-1_0-devel \ + libQt5DBus-devel From a6cf59995621e8b08cebc20ec3c23809b8d9fb5a Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 17:11:48 +0200 Subject: [PATCH 046/110] Add Fedora 34 build. --- .github/workflows/ci-build.yml | 4 +++- docker/Dockerfile.fedora-34 | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfile.fedora-34 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 4cfeb7fd..4e258e84 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -19,6 +19,7 @@ jobs: - fedora-31 - fedora-32 - fedora-33 + - fedora-34 - debian-stretch - debian-buster - ubuntu-18.04 @@ -193,7 +194,8 @@ jobs: ["opensuse-15.1"]="opensuse/15.1" ["opensuse-15.2"]="opensuse/15.2" \ ["opensuse-15.3"]="opensuse/15.3" ["centos-8"]="el/8" \ ["fedora-30"]="fedora/30" ["fedora-31"]="fedora/31" \ - ["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" ) + ["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" \ + ["fedora-34"]="fedora/34" ) export DISTRO=${distromap[${{ matrix.docker_tag }}]} echo PKGTYPE=$PKG_TYPE echo DISTRO=$DISTRO diff --git a/docker/Dockerfile.fedora-34 b/docker/Dockerfile.fedora-34 new file mode 100644 index 00000000..92d641ce --- /dev/null +++ b/docker/Dockerfile.fedora-34 @@ -0,0 +1,20 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM fedora:34 + +RUN mkdir /build +RUN dnf -y install --setopt=install_weak_deps=False --best \ + cmake \ + udev \ + gcc-c++ \ + tar \ + make \ + git \ + qt5-qtdeclarative-devel \ + pkg-config \ + rpm-build \ + qt5-linguist \ + qt5-qtx11extras-devel \ + libusbx-devel + From 3a812179206252022ad32bd29aaf72327a87198a Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 17:16:36 +0200 Subject: [PATCH 047/110] Remove login to container registry. Not necessary anymore. --- .github/workflows/ci-build.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 4e258e84..7733e873 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -58,15 +58,9 @@ jobs: echo Detected branch: ${BRANCH} echo "BRANCH=${BRANCH}" >> $GITHUB_ENV - - name: Login to github docker registry - run: echo ${DOCKER_TOKEN} | docker login ghcr.io -u ${{ secrets.DOCKER_USER }} --password-stdin - env: - DOCKER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Pull ${{ matrix.docker_tag }} docker image run: | docker pull ${DOCKER_IMG}:${{ matrix.docker_tag }} - docker logout ghcr.io - name: docker create build container run: | docker run --name build --env MAKEFLAGS=${MAKEFLAGS} \ From 7c1a45c5b0d92fad7fbe12c9339070d60a372cef Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 18:46:26 +0200 Subject: [PATCH 048/110] Sanity check for invalid shared_ptr and allow empty KeySequenceAction. --- src/deviceinput.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 4ae5b6d5..0c26d799 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -192,7 +192,7 @@ DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], } // KeyEvent in Sequence has action attached, but there are other possible sequences... - if (m_pos->action && !m_pos->action->empty()) { + if (m_pos->action) { return Result::PartialHit; } @@ -213,9 +213,12 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) m_rootItem.nextMap.clear(); m_items.clear(); - // -- fill keymaps + // -- fill keymaps for (const auto& configItem : config) { + // sanity check + if (!configItem.second.action) continue; + KeyEventItem* previous = nullptr; KeyEventItem* current = &m_rootItem; const auto& kes = configItem.first; From c11e55a027e5703c4570329d34f4cad049adcc07 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 20:17:29 +0200 Subject: [PATCH 049/110] Add debian bullseye ci build and dockerfile. --- .github/workflows/ci-build.yml | 1 + docker/Dockerfile.debian-bullseye | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 docker/Dockerfile.debian-bullseye diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 7733e873..9a7cbff5 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -22,6 +22,7 @@ jobs: - fedora-34 - debian-stretch - debian-buster + - debian-bullseye - ubuntu-18.04 - ubuntu-20.04 - ubuntu-20.10 diff --git a/docker/Dockerfile.debian-bullseye b/docker/Dockerfile.debian-bullseye new file mode 100644 index 00000000..371afde1 --- /dev/null +++ b/docker/Dockerfile.debian-bullseye @@ -0,0 +1,20 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM debian:bullseye + +RUN apt-get update +RUN DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + g++ \ + make \ + cmake \ + udev \ + git \ + pkg-config \ + qtdeclarative5-dev \ + qttools5-dev-tools \ + libqt5x11extras5-dev \ + libusb-1.0-0-dev \ + && rm -rf /var/lib/apt/lists/* From 2b6b7e9487e352a4f9c906c6fb3338c31feac62f Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 20:41:38 +0200 Subject: [PATCH 050/110] Prepare/update changelog with changes for 1.0. List might not be complete yet, but contains all the changes up until now that will included when the branch will be merged to develop. --- doc/CHANGELOG.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 0f1ce765..68c90667 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,4 +1,24 @@ -# Changelog +# Projecteur Changelog + +## v1.0 + +### Changes/Updates: + +- Logitech Spotlight Bluetooth vibration & hidraw support ([#140][p140]); +- Logitech Spotlight Scrolling and Audio Volume functionality ([#85][i85]); Many thanks to @mayanksuman +- Bug fix for high CPU load in certain situations ([#133][i133]) +- Bug fix for wrong button mapping for inputs with same length ([#144][i144]) +- Added automated builds for Fedora 34, Debian 11 (Bullseye) and OpenSUSE 15.3 ([#148][i148]) + +Many thanks to *[@mayanksuman][c-mayanksuman]* for Logitech Bluetooth, Scrolling and Audio volume +support. + +[p140]: https://github.com/jahnf/Projecteur/pull/140 +[i85]: https://github.com/jahnf/Projecteur/issues/85 +[i133]: https://github.com/jahnf/Projecteur/issues/133 +[i144]: https://github.com/jahnf/Projecteur/issues/144 +[i148]: https://github.com/jahnf/Projecteur/issues/148 +[c-mayanksuman]: https://github.com/mayanksuman ## v0.9.1 @@ -11,7 +31,7 @@ ### Changes/Updates: -- Added man pages and Appstream files - thanks to @llimeht ([#97][p97]); +- Added man pages and Appstream files - thanks to *[@llimeht][c-llimeht]* ([#97][p97]); - Command line option to toggle the spotlight ([#104][i104]); - Bugfix when moving the cursor from one screen to a different screen with higher resolution; - Multi-screen overlay option ([#80][i80]); @@ -29,6 +49,7 @@ [p115]: https://github.com/jahnf/Projecteur/pull/115 [p113]: https://github.com/jahnf/Projecteur/pull/113 [i6]: https://github.com/jahnf/Projecteur/issues/6 +[c-llimeht]: https://github.com/llimeht ## v0.8 From 285024a7b5e7b2f80c21ba654516ac831fceb1d6 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 20:59:42 +0200 Subject: [PATCH 051/110] Update Linux Repository documentation. Add Fedora 34, Debian Bullseye & OpenSuse 15.3 --- doc/CHANGELOG.md | 2 +- doc/LinuxRepositories.md | 49 ++++++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 68c90667..539564a8 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -5,7 +5,7 @@ ### Changes/Updates: - Logitech Spotlight Bluetooth vibration & hidraw support ([#140][p140]); -- Logitech Spotlight Scrolling and Audio Volume functionality ([#85][i85]); Many thanks to @mayanksuman +- Logitech Spotlight Scrolling and Audio Volume functionality ([#85][i85]); - Bug fix for high CPU load in certain situations ([#133][i133]) - Bug fix for wrong button mapping for inputs with same length ([#144][i144]) - Added automated builds for Fedora 34, Debian 11 (Bullseye) and OpenSUSE 15.3 ([#148][i148]) diff --git a/doc/LinuxRepositories.md b/doc/LinuxRepositories.md index 4b6cf5ca..f7bf0345 100644 --- a/doc/LinuxRepositories.md +++ b/doc/LinuxRepositories.md @@ -43,12 +43,12 @@ and are accessible as a Linux repository for different distributions. See also: * https://cloudsmith.io/~jahnf/repos/projecteur-develop/setup/#formats-deb * https://cloudsmith.io/~jahnf/repos/projecteur-develop/setup/#formats-rpm - + [![Cloudsmith OSS Hosting](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](https://cloudsmith.com) #### Debian Stretch -``` +```sh apt-get install -y debian-keyring apt-get install -y debian-archive-keyring apt-get install -y apt-transport-https @@ -59,7 +59,7 @@ apt-get update #### Debian Buster -``` +```sh apt-get install -y debian-keyring apt-get install -y debian-archive-keyring apt-get install -y apt-transport-https @@ -68,9 +68,20 @@ curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/c apt-get update ``` -#### Ubuntu 18.04 +#### Debian Bullseye +```sh +apt-get install -y debian-keyring +apt-get install -y debian-archive-keyring +apt-get install -y apt-transport-https +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add - +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=debian&codename=bullseye' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list +apt-get update ``` + +#### Ubuntu 18.04 + +```sh apt-get install -y apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add - curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=bionic' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list @@ -79,7 +90,7 @@ apt-get update #### Ubuntu 20.04 -``` +```sh apt-get install -y apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add - curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=focal' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list @@ -88,7 +99,7 @@ apt-get update #### OpenSuse 15.1 -``` +```sh curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.1' > /tmp/jahnf-projecteur-develop.repo zypper ar -f '/tmp/jahnf-projecteur-develop.repo' zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source @@ -96,15 +107,23 @@ zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur- #### OpenSuse 15.2 -``` +```sh curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.2' > /tmp/jahnf-projecteur-develop.repo zypper ar -f '/tmp/jahnf-projecteur-develop.repo' zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source ``` +#### OpenSuse 15.3 + +```sh +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.3' > /tmp/jahnf-projecteur-develop.repo +zypper ar -f '/tmp/jahnf-projecteur-develop.repo' +zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source +``` + #### Fedora 31 - ``` + ```sh dnf install yum-utils pygpgme rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=31' > /tmp/jahnf-projecteur-develop.repo @@ -114,7 +133,7 @@ dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' -- #### Fedora 32 -``` +```sh dnf install yum-utils pygpgme rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=32' > /tmp/jahnf-projecteur-develop.repo @@ -124,7 +143,7 @@ dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' -- #### Fedora 33 -``` +```sh dnf install yum-utils pygpgme rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=33' > /tmp/jahnf-projecteur-develop.repo @@ -132,6 +151,16 @@ dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo' dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source' ``` +#### Fedora 34 + +```sh +dnf install yum-utils pygpgme +rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=34' > /tmp/jahnf-projecteur-develop.repo +dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo' +dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source' +``` + #### CentOS 8 ``` From e1217fd06f1e1e83111cbd749e63159d9a86c5c3 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 21:33:32 +0200 Subject: [PATCH 052/110] Use QtQuick compiler if available. --- CMakeLists.txt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 39fab8f5..7bdd9c7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,12 +30,13 @@ set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) find_package(Qt5 5.7 COMPONENTS Core Gui Quick Widgets REQUIRED) find_package(Qt5 QUIET COMPONENTS X11Extras) set(HAS_Qt5_X11Extras ${Qt5_FOUND}) find_package(Qt5 QUIET COMPONENTS DBus) set(HAS_Qt5_DBus ${Qt5_FOUND}) +find_package(Qt5 QUIET COMPONENTS QuickCompiler REQUIRED) +set(HAS_Qt5_QuickCompiler ${Qt5_FOUND}) # Qt 5.8 seems to have issues with the way Projecteur shows the full screen overlay window, # let's warn the user about it. @@ -45,6 +46,17 @@ if(Qt5_VERSION VERSION_EQUAL "5.8" "please use a different Qt Version.") endif() +if (HAS_Qt5_QuickCompiler) + message(STATUS "Using QtQuick Compiler.") + qtquick_compiler_add_resources(RESOURCES resources.qrc qml/qml.qrc) + # Avoid CMake policy CMP0071 warning + foreach(resfile IN LISTS RESOURCES) + set_property(SOURCE "${resfile}" PROPERTY SKIP_AUTOMOC ON) + endforeach() +else() + qt5_add_resources(RESOURCES resources.qrc qml/qml.qrc) +endif() + add_executable(projecteur src/main.cc src/enum-helper.h src/aboutdlg.cc src/aboutdlg.h @@ -70,7 +82,7 @@ add_executable(projecteur src/spotshapes.cc src/spotshapes.h src/virtualdevice.h src/virtualdevice.cc src/hidpp.cc src/hidpp.h - resources.qrc qml/qml.qrc) + ${RESOURCES}) target_include_directories(projecteur PRIVATE src) From 43c24b62a3927629e1fe73c6a7e8578b0274fba7 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 10 Aug 2021 21:36:27 +0200 Subject: [PATCH 053/110] Fix for required QtQuick compiler. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bdd9c7a..84ba5c84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ find_package(Qt5 QUIET COMPONENTS X11Extras) set(HAS_Qt5_X11Extras ${Qt5_FOUND}) find_package(Qt5 QUIET COMPONENTS DBus) set(HAS_Qt5_DBus ${Qt5_FOUND}) -find_package(Qt5 QUIET COMPONENTS QuickCompiler REQUIRED) +find_package(Qt5 QUIET COMPONENTS QuickCompiler) set(HAS_Qt5_QuickCompiler ${Qt5_FOUND}) # Qt 5.8 seems to have issues with the way Projecteur shows the full screen overlay window, From d68d929b07c2c9d5b26d22dfc3f4ba0e07b2b7b4 Mon Sep 17 00:00:00 2001 From: Jahn Date: Wed, 11 Aug 2021 21:03:50 +0200 Subject: [PATCH 054/110] Fix crash with QDialog::exec(). --- src/projecteurapp.cc | 8 ++++---- src/projecteurapp.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index 3477bd83..16f947fb 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -143,9 +143,9 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio connect(actionAbout, &QAction::triggered, this, [this]() { if (!m_aboutDialog) { - m_aboutDialog = std::make_unique(); - connect(m_aboutDialog.get(), &QDialog::finished, this, [this](int){ - m_aboutDialog.reset(); // No need to keep about dialog in memory, not that important + m_aboutDialog = new AboutDialog(); + connect(m_aboutDialog, &QDialog::finished, this, [this](int){ + m_aboutDialog->deleteLater(); // No need to keep about dialog in memory, not that important }); } @@ -154,7 +154,7 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio m_aboutDialog->raise(); m_aboutDialog->activateWindow(); } else { - m_aboutDialog->exec(); + m_aboutDialog->open(); } }); diff --git a/src/projecteurapp.h b/src/projecteurapp.h index 1c4ad224..f1a6acdf 100644 --- a/src/projecteurapp.h +++ b/src/projecteurapp.h @@ -70,7 +70,7 @@ private slots: std::unique_ptr m_trayIcon; std::unique_ptr m_trayMenu; std::unique_ptr m_dialog; - std::unique_ptr m_aboutDialog; + QPointer m_aboutDialog; QLocalServer* const m_localServer = nullptr; Spotlight* m_spotlight = nullptr; Settings* m_settings = nullptr; From 8200995098dece922eca463e5f6dd2c79a882889 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Wed, 11 Aug 2021 19:35:36 +0530 Subject: [PATCH 055/110] Added documentation for Spotlight HID++ Additionally, randomized the software identification code to better match official HID++ protocol. --- doc/LogitechSpotlightHID++.md | 251 ++++++++++++++++++++++++++++++++++ src/device.cc | 62 +++++---- src/hidpp.cc | 60 ++++---- src/hidpp.h | 8 +- src/spotlight.cc | 23 ++-- 5 files changed, 331 insertions(+), 73 deletions(-) create mode 100644 doc/LogitechSpotlightHID++.md diff --git a/doc/LogitechSpotlightHID++.md b/doc/LogitechSpotlightHID++.md new file mode 100644 index 00000000..e8c73ff2 --- /dev/null +++ b/doc/LogitechSpotlightHID++.md @@ -0,0 +1,251 @@ +# HID++ Basics + +There are two major version of Logitech HID++ protocol. The Logitech spotlight +supports HID++ protocol version 2.0+. This document provide information about +HID++ version 2.0+ only. In the HID++ protocol two different types of messages +can be used for communication to/from Logitech Spotlight device. +These two types of message are + + 1. Short Message: 7 bytes long. Default message scheme for USB Dongle. The + Spotlight device only supports short messages, when it is connected through + the USB dongle. + + * First Byte: `0x10` + + * Second Byte: Device code for which the message is meant (in case it + is sent from PC)/originated. 0xff for USB dongle, `0x01` for + Logitech Spotlight device. + + * Third Byte: Feature Index. Some of the featureIndex are `0x00` (for + Root feature: used for querying device details), + `0x80` (short set), `0x81` (short get). + + * Forth Byte: If third byte is not `0x80` or `0x81` then last 4 bits + (forth_byte & 0xf0) are function code and first 4 bits + (forth_byte & 0x0f) are software identification code. + Software identification code is random value in range of 0 to 15 + (used to differentiate traffic for different softwares). + + * Fifth - Seventh Bytes: Parameters/data + + 2. Long Message: 20 bytes long. Logitech Spotlight supports long messages in + any connection mode (through USB dongle and Bluetooth). In long messages, the + first byte is `0x11`, the next three bytes (second byte to forth byte) are the + same as in short messages. However, in long messages, there are additional + bytes (Fifth - Twentieth bytes) that can be used as parameters/data. + +Please note that the device response will have the first four bytes the same as +in the request message, if no error is reported. However, in case of an error, +the third byte in the device response will be `0x8f` and first, second, forth +and fifth byte in the device response will be same as the first, second, third +and forth byte respectively as in the request message from the application. + +If the Spotlight device is connected through Bluetooth then a short HID++ +message meant for device should be transformed to a long HID++ message before +sending it to device. For changing a short message to a long message, the first +byte is replaced as 0x11 and the message is appended with trailing zero to +achieve the length of 20. + +## HID++ Feature Code + +HID++ feature codes (of type `uint16_t`; 2^16 possible feature codes) are +defined for a set of all the possible features supported by any Logitech HID++ +device produced up until today. The feature code is part of the HID++ protocol +and does not vary for different devices. Some of the well known HID++2 feature +codes are: + +| `Feature Code Name` | `Byte Value` | +| -------------------------- | ------------:| +| `ROOT` | `0x0000` | +| `FEATURE_SET` | `0x0001` | +| `FEATURE_INFO` | `0x0002` | +| `DEVICE_FW_VERSION` | `0x0003` | +| `DEVICE_UNIT_ID` | `0x0004` | +| `DEVICE_NAME` | `0x0005` | +| `DEVICE_GROUPS` | `0x0006` | +| `DEVICE_FRIENDLY_NAME` | `0x0007` | +| `KEEP_ALIVE` | `0x0008` | +| `RESET` | `0x0020` | +| `CRYPTO_ID` | `0x0021` | +| `TARGET_SOFTWARE` | `0x0030` | +| `WIRELESS_SIGNAL_STRENGTH` | `0x0080` | +| `DFUCONTROL_LEGACY` | `0x00C0` | +| `DFUCONTROL_UNSIGNED` | `0x00C1` | +| `DFUCONTROL_SIGNED` | `0x00C2` | +| `DFU` | `0x00D0` | + +A more extensive list of known feature codes are documented by the +[Solaar project](https://github.com/pwr-Solaar/Solaar/blob/master/docs/features.md). +Some of the feature codes relevant for the Logitech Spotlight are defined in +[hidpp.h](../src/hidpp.h). + +```c++ +enum class FeatureCode : uint16_t { + Root = 0x0000, + FeatureSet = 0x0001, + FirmwareVersion = 0x0003, + DeviceName = 0x0005, + Reset = 0x0020, + DFUControlSigned = 0x00c2, + BatteryStatus = 0x1000, + PresenterControl = 0x1a00, + Sensor3D = 0x1a01, + ReprogramControlsV4 = 0x1b04, + WirelessDeviceStatus = 0x1db4, + SwapCancelButton = 0x2005, + PointerSpeed = 0x2205, +}; +``` + +No single Logitech device supports all feature codes. Rather, a device supports +a limited range of features and corresponding feature codes. Inside the device, +the supported feature codes are mapped to an index (or Feature Index). This +mapping is called FeatureSet table. For any device, the Root Feature Code (`0x0000`) +has an index of `0x00`. Root Feature Index (`0x00`) is used for getting the +entire FeatureSet, and pinging the device. + +For any device, the feature index corresponding to any feature code can be +obtained by using Root Feature Index (`0x00`) by sending the request message +`{0x10, 0x01, 0x00, 0x0d, Feature_Code(2 bytes), 0x00}` (here, the function +code is `0x00` and the software identification code is `0x0d` in forth byte). +If the return message is not an error message, the fifth byte in the response +is the Feature index and the 6th byte is the Feature type (See below). + +The application can retrieve the entire FeatureSet table for the device with +following steps: + + 1. Get the number of features supported by device: + + a. Get the _Feature Index_ corresponding to the FeatureSet code (`0x0001`). + + b. Get the number of features supported by sending the request message + `{0x10, 0x01, (FeatureSet Index), 0x0d, 0x00, 0x00, 0x00}` + (3rd byte is the Feature Index for FeatureSet Code; function code + is `0x00` and software identification code is `0x0d` in forth byte). + In the response, the 5th byte will be the number of features + supported, except the root feature. As stated above, Root feature + always has the Feature Index `0x00`. Hence, total number of features + supported is one plus the count obtained in the response. + + 2. Iterate over the Feature Indexes 1 to the number of features supported and + send the request (assuming Feature_Index for feature set is `0x01`; third byte) + `{0x10, 0x01, 0x01, 0x1d, Feature_Index, 0x00, 0x00}` (function code is `0x10` + and software identification code is `0x0d` in forth byte). The response will + contain the HID++ Feature Code at byte 5 and 6 as `uint16_t` and the Feature + Type at byte 7 for a valid Feature Index. In the Feature Type byte, if 7th bit + is set this means _Software Hidden_, if bit 8 is set this means + _Obsolete feature_. So, Software_Hidden = (Feature_Type & (1<<6)) and + Obsolete_Feature = (Feature_Type & (1<<7)). + Software Hidden or Obsolete features should not be handled by any application. + In case the Feature Index is not valid (i.e.,feature index > number of + feature supported) then `0x0200` will be in the response at byte 5 and 6. + +The FeatureSet table for a device may change with a firmware update. The +application should cache FeatureSet table along with Firmware version and only +read FeatureSet table again if the firmware version has changed. This logic for +getting FeatureSet table from device is implemented in +`populateFeatureTable` method in `FeatureSet` class in [hidpp.h](../src/hidpp.h). + +# Resetting Logitech Spotlight device + +Depending on the connection mode (USB dongle or Bluetooth), the Logitech +Spotlight device can be reset with following HID++ message from the application: + + 1. Reset the USB dongle by sending following commands in sequence +``` + {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00} //get wireless notification and software connection status + {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00} // set sofware bit to false + {0x10, 0xff, 0x80, 0x02, 0x02, 0x00, 0x00} // initialize the USB dongle + {0x10, 0xff, 0x80, 0x00, 0x00, 0x09, 0x00} // set sofware bit to true +``` + + 2. Load the FeatureSet table for the device (from pre-existing cache or from + the device if firmware version has changed by calling `populateFeatureTable` + method in `FeatureSet` class in [hidpp.h](../src/hidpp.h)). + + 3. Reset the Spotlight device with the Feature index for Reset Feature Code + from the FeatureSet table. If the Feature Index for Reset Feature Code is + `0x05`, then HID++ request message for resetting will be + `{0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}`. + +In addition to these steps, the Projecteur also pings the device by sending +`{0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}` (function code `0x10` and software +identification code `0x0d` in forth byte). The response to this ping contains +the HID++ version supported by the device. + +Further, Projecteur configures the Logitech device to send `Next Hold` and +`Back Hold` events and resets the pointer speed to a default value with +following HID++ commands: + +``` +// enable next button hold (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xda - next button, 0x33 - hold event) +{0x11, 0x01, 0x07, 0x3d, 0x00, 0xda, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} +// back button hold (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xdc - back button, 0x33 - hold event) +{0x11, 0x01, 0x07, 0x3d, 0x00, 0xdc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} +// Reset pointer Speed (0x0a - Feature Index for Reset Feature Code, 0x14 - 5th level pointer speeed (can be between 0x10-0x19)) +{0x10, 0x01, 0x0a, 0x1d, 0x14, 0x00, 0x00} +``` + +These initialization steps are implemented in `initialize` method of +`SubHidppConnection` class in [device.h](../src/device.h). After reprogramming +the Next and Back buttons, the first and second response of the spotlight +device contains relative mouse move data through HID++ messages +(not regular input events to the OS). These events are handled in `onHidppDataAvailable` +method in `Spotlight` class in [spotlight.h](../src/spotlight.h). Special +'repeated' actions (like scrolling and volume control) can utilize the relative +mouse movement data received in the responses. + +For completeness, it should be noted that the official Logitech Spotlight +software reprogram the click and double click events too by following HID++ +commands: + +``` +// Send click event as HID++ message (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xd8-click button, 0x33- hold event) +{0x11, 0x01, 0x07, 0x3d, 0x00, 0xd8, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} +// Send double click as HID++ message (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xdf - double click) +{0x11, 0x01, 0x07, 0x3d, 0x00, 0xdf, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} +``` + +Projecteur does not send these packages. Instead, it grab the events from mouse +event device associated with Spotlight. The approach taken by Projecteur is +advantageous as it help in implementing Input Mapping feature that official +Logitech Software lacks. However, this approach also makes porting Projecteur +to different platforms more difficult. + +# Wireless Notification on Activation/Deactivation of Spotlight Device + +The Spotlight device sends a wireless notification if it gets activated. +Wireless notification will be short HID++ message if the Spotlight device is +connected through USB. Otherwise, it will be a long HID++ message. + +For short HID++ wireless notifications, the third byte will be `0x41`. In this +message, the 6th bit in 5th byte shows the activation status of spotlight +device. If the 6th bit is 0 then device just got active, otherwise the device +just got deactivated. + +A long HID++ wireless notification is only received for device activation. +In this message, third byte will be the Feature Index for the Wireless +Notification Feature Code (`0x1db4`). + +# Vibration support + +The spotlight device can vibrate if the HID++ message +`{0x10, 0x01, (Feature Index for Presenter Control Feature Code), 0x1d, length, 0xe8, intensity}` +is sent to it. In the message, length can range between `0x00` to `0x0a`. + +# Processing of device response + +All of the HID++ commands listed above result in response messages from the +Spotlight device. For most messages, these responses from device are just the +acknowledgements of the HID++ commands sent by the application. However, some +responses from the Spotlight device contain useful information. For details on +processing such responses see the `onHidppDataAvailable` method in the +`Spotlight` class in [spotlight.h](../src/spotlight.h). + +# Further information + +For more information about HID++ protocol in general please check +[logitech-hidpp module](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-logitech-hidpp.c) +code in linux kernel source. Documentation from +[Solaar project](https://github.com/pwr-Solaar/Solaar/blob/master/docs/) +might be helpful too. diff --git a/src/device.cc b/src/device.cc index 17e5904d..2ad66a55 100644 --- a/src/device.cc +++ b/src/device.cc @@ -370,8 +370,9 @@ void SubDeviceConnection::queryBatteryStatus() {} // ------------------------------------------------------------------------------------------------- void SubHidppConnection::pingSubDevice() { - constexpr uint8_t rootID = 0x00; // root ID is always 0x00 in any logitech device - const uint8_t pingCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rootID, 0x1d, 0x00, 0x00, 0x5d}; + constexpr uint8_t rootIndex = 0x00; // root Index is always 0x00 in any logitech device + const uint8_t pingCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rootIndex, + m_featureSet->getRandomFunctionCode(0x10), 0x00, 0x00, 0x5d}; sendData(pingCmd, sizeof(pingCmd)); } @@ -464,10 +465,10 @@ void SubHidppConnection::initialize() // Reset spotlight device if (m_featureSet->getFeatureCount()) { - const auto resetID = m_featureSet->getFeatureID(FeatureCode::Reset); - if (resetID) { - QTimer::singleShot(delay_ms*msgCount, this, [this, resetID](){ - const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, resetID, 0x1d, 0x00, 0x00, 0x00}; + const auto resetIndex = m_featureSet->getFeatureIndex(FeatureCode::Reset); + if (resetIndex) { + QTimer::singleShot(delay_ms*msgCount, this, [this, resetIndex](){ + const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, resetIndex, m_featureSet->getRandomFunctionCode(0x10), 0x00, 0x00, 0x00}; sendData(data, sizeof(data));}); msgCount++; } @@ -488,18 +489,20 @@ void SubHidppConnection::initialize() } // Enable Next and back button on hold functionality. - const auto rcID = m_featureSet->getFeatureID(FeatureCode::ReprogramControlsV4); - if (rcID) { + const auto rcIndex = m_featureSet->getFeatureIndex(FeatureCode::ReprogramControlsV4); + if (rcIndex) { if (hasFlags(DeviceFlags::NextHold)) { - QTimer::singleShot(delay_ms*msgCount, this, [this, rcID](){ - const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcID, 0x3d, 0x00, 0xda, 0x33}; + QTimer::singleShot(delay_ms*msgCount, this, [this, rcIndex](){ + const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcIndex, m_featureSet->getRandomFunctionCode(0x30), 0x00, 0xda, 0x33, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; sendData(data, sizeof(data));}); msgCount++; } if (hasFlags(DeviceFlags::BackHold)) { - QTimer::singleShot(delay_ms*msgCount, this, [this, rcID](){ - const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcID, 0x3d, 0x00, 0xdc, 0x33}; + QTimer::singleShot(delay_ms*msgCount, this, [this, rcIndex](){ + const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcIndex, m_featureSet->getRandomFunctionCode(0x30), 0x00, 0xdc, 0x33, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; sendData(data, sizeof(data));}); msgCount++; } @@ -611,14 +614,8 @@ std::shared_ptr SubHidppConnection::create(const DeviceScan: auto connection = std::make_shared(Token{}, sd, dc.deviceId()); - // TODO feature set needs to be a member of a sub hidraw connection - // connection->m_featureSet = dc.getFeatureSet(); + if (dc.hasHidppSupport()) connection->m_details.deviceFlags |= DeviceFlag::Hidpp; connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); - // --- - - if (dc.hasHidppSupport()) { - connection->m_details.deviceFlags |= DeviceFlag::Hidpp; - } connection->createSocketNotifiers(devfd); connection->m_inputMapper = dc.inputMapper(); @@ -629,6 +626,8 @@ std::shared_ptr SubHidppConnection::create(const DeviceScan: // ------------------------------------------------------------------------------------------------- void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) { + if (!hasFlags(DeviceFlags::Vibrate)) return; + // TODO put in HIDPP // TODO generalize features and protocol for proprietary device features like vibration // for not only the Spotlight device. @@ -638,10 +637,12 @@ void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) // controlID len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; - const uint8_t pcID = getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); - if (pcID == 0x00) return; - const uint8_t vibrateCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, pcID, 0x1d, length, 0xe8, intensity}; - sendData(vibrateCmd, sizeof(vibrateCmd)); + length = length > 10? 10: length; //length should be between 0 to 10. + const uint8_t pcIndex = getFeatureSet()->getFeatureIndex(FeatureCode::PresenterControl); + using namespace HIDPP; + const uint8_t vibrateCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, pcIndex, + m_featureSet->getRandomFunctionCode(0x10), length, 0xe8, intensity}; + if (pcIndex) sendData(vibrateCmd, sizeof (vibrateCmd)); } // ------------------------------------------------------------------------------------------------- @@ -650,24 +651,27 @@ void SubHidppConnection::queryBatteryStatus() // TODO put parts in HIDPP if (hasFlags(DeviceFlag::ReportBattery)) { - const uint8_t batteryFeatureID = m_featureSet->getFeatureID(FeatureCode::BatteryStatus); - if (batteryFeatureID) + const uint8_t batteryFeatureIndex = m_featureSet->getFeatureIndex(FeatureCode::BatteryStatus); + if (batteryFeatureIndex) { - const uint8_t batteryCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, batteryFeatureID, 0x0d, 0x00, 0x00, 0x00}; + const uint8_t batteryCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, batteryFeatureIndex, + m_featureSet->getRandomFunctionCode(0x00), 0x00, 0x00, 0x00}; sendData(batteryCmd, sizeof(batteryCmd)); } setWriteNotifierEnabled(false); } } +// ------------------------------------------------------------------------------------------------- void SubHidppConnection::setPointerSpeed(uint8_t level) { - const uint8_t psID = getFeatureSet()->getFeatureID(FeatureCode::PointerSpeed); - if (psID == 0x00) return; + const uint8_t psIndex = getFeatureSet()->getFeatureIndex(FeatureCode::PointerSpeed); + if (psIndex == 0x00) return; level = (level > 0x09) ? 0x09: level; // level should be in range of 0-9 uint8_t pointerSpeed = 0x10 & level; // pointer speed sent to device are between 0x10 - 0x19 (hence ten speed levels) - const uint8_t pointerSpeedCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, psID, 0x1d, pointerSpeed, 0x00, 0x00}; + const uint8_t pointerSpeedCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, psIndex, + m_featureSet->getRandomFunctionCode(0x10), pointerSpeed, 0x00, 0x00}; sendData(pointerSpeedCmd, sizeof(pointerSpeedCmd)); } diff --git a/src/hidpp.cc b/src/hidpp.cc index e195f86d..ae726afa 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -28,7 +28,7 @@ QByteArray FeatureSet::getResponseFromDevice(const QByteArray& expectedBytes) QByteArray readVal(20, 0); int timeOut = 4; // time out just in case device did not reply; - // 4 seconds time out is used by other prorams like Solaar. + // 4 seconds time out is used by other programs like Solaar. QTime timeOutTime = QTime::currentTime().addSecs(timeOut); while(true) { if(::read(m_fdHIDDevice, readVal.data(), readVal.length())) { @@ -40,7 +40,7 @@ QByteArray FeatureSet::getResponseFromDevice(const QByteArray& expectedBytes) } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureIDFromDevice(FeatureCode fc) +uint8_t FeatureSet::getFeatureIndexFromDevice(FeatureCode fc) { if (m_fdHIDDevice == -1) return 0x00; @@ -48,8 +48,8 @@ uint8_t FeatureSet::getFeatureIDFromDevice(FeatureCode fc) const uint8_t fSetMSB = static_cast(static_cast(fc)); const auto featureReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, 0x00, 0x0d, fSetLSB, fSetMSB, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, 0x00, getRandomFunctionCode(0x00), + fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); const auto res = ::write(m_fdHIDDevice, featureReqMessage.data(), featureReqMessage.size()); @@ -61,20 +61,20 @@ uint8_t FeatureSet::getFeatureIDFromDevice(FeatureCode fc) const auto response = getResponseFromDevice(featureReqMessage.mid(1, 3)); if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; - uint8_t featureID = static_cast(response.at(4)); + uint8_t featureIndex = static_cast(response.at(4)); - return featureID; + return featureIndex; } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t featureSetID) +uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t featureSetIndex) { if (m_fdHIDDevice == -1) return 0x00; // Get Number of features (except Root Feature) supported const auto featureCountReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x00), + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); const auto res = ::write(m_fdHIDDevice, featureCountReqMessage.data(), featureCountReqMessage.size()); @@ -96,14 +96,14 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() { if (m_fdHIDDevice == -1) return 0x00; - // To get firmware details: first get Feature ID corresponding to Firmware feature code - uint8_t fwID = getFeatureIDFromDevice(FeatureCode::FirmwareVersion); - if (!fwID) return QByteArray(); + // To get firmware details: first get Feature Index corresponding to Firmware feature code + uint8_t fwIndex = getFeatureIndexFromDevice(FeatureCode::FirmwareVersion); + if (!fwIndex) return QByteArray(); // Get the number of firmwares (Main HID++ application, BootLoader, or Hardware) now const auto fwCountReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwID, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x00), + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); const auto res = ::write(m_fdHIDDevice, fwCountReqMessage.data(), fwCountReqMessage.size()); @@ -136,8 +136,8 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() for (uint8_t i = 0x00; i < fwCount; i++) { const auto fwVerReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwID, 0x1d, i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x10), + i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); const auto res = ::write(m_fdHIDDevice, fwVerReqMessage.data(), fwVerReqMessage.length()); @@ -186,22 +186,22 @@ void FeatureSet::populateFeatureTable() else { // For reading feature table from device - // first get featureID for FeatureCode::FeatureSet + // first get featureIndex for FeatureCode::FeatureSet // then we can get the number of features supported by the device (except Root Feature) - const uint8_t featureSetID = getFeatureIDFromDevice(FeatureCode::FeatureSet); - if (!featureSetID) return; - const uint8_t featureCount = getFeatureCountFromDevice(featureSetID); + const uint8_t featureSetIndex = getFeatureIndexFromDevice(FeatureCode::FeatureSet); + if (!featureSetIndex) return; + const uint8_t featureCount = getFeatureCountFromDevice(featureSetIndex); if (!featureCount) return; - // Root feature is supported by all HID++ 2.0 device and has a featureID of 0 always. + // Root feature is supported by all HID++ 2.0 device and has a featureIndex of 0 always. m_featureTable.insert({static_cast(FeatureCode::Root), 0x00}); - // Read Feature Code for other featureIds from device. - for (uint8_t featureId = 0x01; featureId <= featureCount; ++featureId) + // Read Feature Code for other featureIndices from device. + for (uint8_t featureIndex = 0x01; featureIndex <= featureCount; ++featureIndex) { const auto featureCodeReqMsg = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetID, 0x1d, featureId, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x10), featureIndex, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); const auto res = ::write(m_fdHIDDevice, featureCodeReqMsg.data(), featureCodeReqMsg.size()); if (res != featureCodeReqMsg.size()) { @@ -218,7 +218,7 @@ void FeatureSet::populateFeatureTable() const uint8_t featureType = static_cast(response.at(6)); const auto softwareHidden = (featureType & (1<<6)); const auto obsoleteFeature = (featureType & (1<<7)); - if (!(softwareHidden) && !(obsoleteFeature)) m_featureTable.insert({featureCode, featureId}); + if (!(softwareHidden) && !(obsoleteFeature)) m_featureTable.insert({featureCode, featureIndex}); } } } @@ -231,12 +231,12 @@ bool FeatureSet::supportFeatureCode(FeatureCode fc) const } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureID(FeatureCode fc) const +uint8_t FeatureSet::getFeatureIndex(FeatureCode fc) const { if (!supportFeatureCode(fc)) return 0x00; - const auto featurePair = m_featureTable.find(static_cast(fc)); - return featurePair->second; + const auto featureInfo = m_featureTable.find(static_cast(fc)); + return featureInfo->second; } // ------------------------------------------------------------------------------------------------- @@ -277,4 +277,4 @@ bool isMessageForUsb(const QByteArray& msg) { return (static_cast(msg.at(1)) == Bytes::MSG_TO_USB_RECEIVER); } -} // end namespace HIDPP \ No newline at end of file +} // end namespace HIDPP diff --git a/src/hidpp.h b/src/hidpp.h index afa4855f..838b5d76 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -54,19 +54,21 @@ namespace HIDPP { { public: void setHIDDeviceFileDescriptor(int fd) { m_fdHIDDevice = fd; } - uint8_t getFeatureID(FeatureCode fc) const; + uint8_t getFeatureIndex(FeatureCode fc) const; bool supportFeatureCode(FeatureCode fc) const; auto getFeatureCount() const { return m_featureTable.size(); } + uint8_t getRandomFunctionCode(uint8_t functionCode) const { return (functionCode | m_softwareIDBits); } void populateFeatureTable(); private: - uint8_t getFeatureIDFromDevice(FeatureCode fc); + uint8_t getFeatureIndexFromDevice(FeatureCode fc); uint8_t getFeatureCountFromDevice(uint8_t featureSetID); QByteArray getFirmwareVersionFromDevice(); QByteArray getResponseFromDevice(const QByteArray &expectedBytes); std::map m_featureTable; int m_fdHIDDevice = -1; + uint8_t m_softwareIDBits = (rand() & 0x0f); }; -} +} //end of HIDPP namespace diff --git a/src/spotlight.cc b/src/spotlight.cc index d5635756..15424346 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -428,30 +428,31 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) if (readVal.at(0) == HIDPP::Bytes::LONG_MSG) // Logitech HIDPP LONG message: 20 byte long { // response to ping - auto rootID = connection.getFeatureSet()->getFeatureID(FeatureCode::Root); - if (readVal.at(2) == rootID) { - if (readVal.at(3) == 0x1d && readVal.at(6) == 0x5d) { + auto rootIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::Root); + if (readVal.at(2) == rootIndex) { + if (readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10) && readVal.at(6) == 0x5d) { auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; connection.setHIDppProtocol(protocolVer); } } // Wireless Notification from the Spotlight device - auto wirelessNotificationID = connection.getFeatureSet()->getFeatureID(FeatureCode::WirelessDeviceStatus); - if (wirelessNotificationID && readVal.at(2) == wirelessNotificationID) { // Logitech spotlight presenter unit got online. + auto wnIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::WirelessDeviceStatus); + if (wnIndex && readVal.at(2) == wnIndex) { // Logitech spotlight presenter unit got online. if (!connection.isOnline()) connection.initialize(); } // Battery packet processing: Device responded to BatteryStatus (0x1000) packet - auto batteryID = connection.getFeatureSet()->getFeatureID(FeatureCode::BatteryStatus); - if (batteryID && readVal.at(2) == batteryID && readVal.at(3) == 0x0d) { // Battery information packet + auto batteryIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::BatteryStatus); + if (batteryIndex && readVal.at(2) == batteryIndex && + readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x00)) { // Battery information packet QByteArray batteryData(readVal.mid(4, 3)); emit connection.receivedBatteryInfo(batteryData); } // Process reprogrammed keys : Next Hold and Back Hold - auto reprogrammedControlID = connection.getFeatureSet()->getFeatureID(FeatureCode::ReprogramControlsV4); - if (reprogrammedControlID && readVal.at(2) == reprogrammedControlID) // Button (for which hold events are on) related message. + auto rcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::ReprogramControlsV4); + if (rcIndex && readVal.at(2) == rcIndex) // Button (for which hold events are on) related message. { auto eventCode = static_cast(readVal.at(3)); auto buttonCode = static_cast(readVal.at(5)); @@ -507,8 +508,8 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) } // Vibration response check - const uint8_t pcID = connection.getFeatureSet()->getFeatureID(FeatureCode::PresenterControl); - if (pcID && readVal.at(2) == pcID && readVal.at(3) == 0x1d) { + const uint8_t pcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::PresenterControl); + if (pcIndex && readVal.at(2) == pcIndex && readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10)) { logDebug(hid) << "Device acknowledged a vibration event."; } } From 0cf25df8dabf651441103b6736517b5d50b47bbc Mon Sep 17 00:00:00 2001 From: Jahn Date: Wed, 11 Aug 2021 21:25:53 +0200 Subject: [PATCH 056/110] Add build type to version info. Additionally don't use QtQuick compiler by default because of possible side effects. --- CMakeLists.txt | 25 ++++++++++++++++--------- cmake/modules/GitVersion.cc.in | 1 + cmake/modules/GitVersion.cmake | 6 ++++++ cmake/modules/GitVersion.h.in | 1 + src/main.cc | 1 + 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 84ba5c84..358b2aa0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,15 +47,22 @@ if(Qt5_VERSION VERSION_EQUAL "5.8" endif() if (HAS_Qt5_QuickCompiler) - message(STATUS "Using QtQuick Compiler.") - qtquick_compiler_add_resources(RESOURCES resources.qrc qml/qml.qrc) - # Avoid CMake policy CMP0071 warning - foreach(resfile IN LISTS RESOURCES) - set_property(SOURCE "${resfile}" PROPERTY SKIP_AUTOMOC ON) - endforeach() -else() - qt5_add_resources(RESOURCES resources.qrc qml/qml.qrc) + # Off by default, since this ties the application strictly to the Qt version + # it is built with, see https://doc.qt.io/qt-5.12/qtquick-deployment.html#compiling-qml-ahead-of-time + option(USE_QTQUICK_COMPILER "Use the QtQuickCompiler" OFF) + + if (USE_QTQUICK_COMPILER) + message(STATUS "Using QtQuick Compiler.") + qtquick_compiler_add_resources(RESOURCES qml/qml.qrc) + # Avoid CMake policy CMP0071 warning + foreach(resfile IN LISTS RESOURCES) + set_property(SOURCE "${resfile}" PROPERTY SKIP_AUTOMOC ON) + endforeach() + else() + qt5_add_resources(RESOURCES qml/qml.qrc) + endif() endif() +qt5_add_resources(RESOURCES resources.qrc) add_executable(projecteur src/main.cc src/enum-helper.h @@ -67,6 +74,7 @@ add_executable(projecteur src/deviceinput.cc src/deviceinput.h src/devicescan.cc src/devicescan.h src/deviceswidget.cc src/deviceswidget.h + src/hidpp.cc src/hidpp.h src/linuxdesktop.cc src/linuxdesktop.h src/iconwidgets.cc src/iconwidgets.h src/imageitem.cc src/imageitem.h @@ -81,7 +89,6 @@ add_executable(projecteur src/spotlight.cc src/spotlight.h src/spotshapes.cc src/spotshapes.h src/virtualdevice.h src/virtualdevice.cc - src/hidpp.cc src/hidpp.h ${RESOURCES}) target_include_directories(projecteur PRIVATE src) diff --git a/cmake/modules/GitVersion.cc.in b/cmake/modules/GitVersion.cc.in index ec137733..561555d0 100644 --- a/cmake/modules/GitVersion.cc.in +++ b/cmake/modules/GitVersion.cc.in @@ -11,4 +11,5 @@ namespace @TARGET@ { const char* version_fullhash() { return "@VERSION_FULLHASH@"; } bool version_isdirty() { return @VERSION_ISDIRTY@; } const char* version_branch() { return "@VERSION_BRANCH@"; } + const char* version_buildtype() { return "@VERSION_BUILDTYPE@"; } } diff --git a/cmake/modules/GitVersion.cmake b/cmake/modules/GitVersion.cmake index 6ff5c89d..de6f43d0 100644 --- a/cmake/modules/GitVersion.cmake +++ b/cmake/modules/GitVersion.cmake @@ -64,6 +64,7 @@ function(get_version_info prefix directory) set(${prefix}_VERSION_STRING 0.0.0-unknown) set(${prefix}_VERSION_STRING_FULL 0.0.0-unknown) set(${prefix}_VERSION_ISDIRTY 0 PARENT_SCOPE) + set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}" PARENT_SCOPE) set(${prefix}_VERSION_DATE_MONTH_YEAR "" PARENT_SCOPE) if("${${prefix}_OR_VERSION_MAJOR}" STREQUAL "") @@ -76,6 +77,8 @@ function(get_version_info prefix directory) set(${prefix}_OR_VERSION_PATCH 0) endif() + set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}") + find_package(Git) if(GIT_FOUND) # Get the version info from the last tag @@ -354,6 +357,7 @@ function(add_version_info_custom_prefix target prefix directory) set(VERSION_STRING "0.0-unknown.0") set(VERSION_STRING_FULL "0.0.0-unknown.0") set(VERSION_ISDIRTY 0) + set(VERSION_BUILDTYPE "unknown") set(VERSION_BRANCH unknown) set(output_dir "${CMAKE_CURRENT_BINARY_DIR}/version/${targetid}") @@ -462,6 +466,7 @@ function(add_version_info_custom_prefix target prefix directory) set(VERSION_STRING ${${prefix}_VERSION_STRING}) set(VERSION_STRING_FULL ${${prefix}_VERSION_STRING_FULL}) set(VERSION_ISDIRTY ${${prefix}_VERSION_ISDIRTY}) + set(VERSION_BUILDTYPE ${${prefix}_VERSION_BUILDTYPE}) set(VERSION_BRANCH ${${prefix}_VERSION_BRANCH}) set(VERSION_DATE_MONTH_YEAR ${${prefix}_VERSION_DATE_MONTH_YEAR}) @@ -481,6 +486,7 @@ function(add_version_info_custom_prefix target prefix directory) VERSION_STRING "${VERSION_STRING}" VERSION_STRING_FULL "${VERSION_STRING_FULL}" VERSION_ISDIRTY "${VERSION_ISDIRTY}" + VERSION_BUILDTYPE "${VERSION_BUILDTYPE}" VERSION_BRANCH "${VERSION_BRANCH}" VERSION_DATE_MONTH_YEAR "${VERSION_DATE_MONTH_YEAR}" ) diff --git a/cmake/modules/GitVersion.h.in b/cmake/modules/GitVersion.h.in index 16b7431d..25f693f2 100644 --- a/cmake/modules/GitVersion.h.in +++ b/cmake/modules/GitVersion.h.in @@ -11,4 +11,5 @@ namespace @TARGET@ { const char* version_fullhash(); bool version_isdirty(); const char* version_branch(); + const char* version_buildtype(); } diff --git a/src/main.cc b/src/main.cc index a316f843..51da7831 100644 --- a/src/main.cc +++ b/src/main.cc @@ -206,6 +206,7 @@ int main(int argc, char *argv[]) { print() << " - compiler: " << XSTRINGIFY(CXX_COMPILER_ID) << " " << XSTRINGIFY(CXX_COMPILER_VERSION); + print() << " - build-type: " << projecteur::version_buildtype(); print() << " - qt-version: (build: " << QT_VERSION_STR << ", runtime: " << qVersion() << ")"; const auto result = DeviceScan::getDevices(options.additionalDevices); From 3405ae50e27bd260c64985cb27849971f55e6192 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Fri, 13 Aug 2021 00:56:00 +0530 Subject: [PATCH 057/110] Updated HID++ doc The documentation for Battery Status check and HID++ during Hold events are added. --- doc/LogitechSpotlightHID++.md | 71 ++++++++++++++++++++++++++++------- src/spotlight.cc | 9 +++-- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/doc/LogitechSpotlightHID++.md b/doc/LogitechSpotlightHID++.md index e8c73ff2..40b65b3c 100644 --- a/doc/LogitechSpotlightHID++.md +++ b/doc/LogitechSpotlightHID++.md @@ -13,7 +13,7 @@ These two types of message are * First Byte: `0x10` * Second Byte: Device code for which the message is meant (in case it - is sent from PC)/originated. 0xff for USB dongle, `0x01` for + is sent from PC)/originated. `0xff` for USB dongle, `0x01` for Logitech Spotlight device. * Third Byte: Feature Index. Some of the featureIndex are `0x00` (for @@ -21,8 +21,8 @@ These two types of message are `0x80` (short set), `0x81` (short get). * Forth Byte: If third byte is not `0x80` or `0x81` then last 4 bits - (forth_byte & 0xf0) are function code and first 4 bits - (forth_byte & 0x0f) are software identification code. + (`forth_byte & 0xf0`) are function code and first 4 bits + (`forth_byte & 0x0f`) are software identification code. Software identification code is random value in range of 0 to 15 (used to differentiate traffic for different softwares). @@ -43,7 +43,7 @@ and forth byte respectively as in the request message from the application. If the Spotlight device is connected through Bluetooth then a short HID++ message meant for device should be transformed to a long HID++ message before sending it to device. For changing a short message to a long message, the first -byte is replaced as 0x11 and the message is appended with trailing zero to +byte is replaced as `0x11` and the message is appended with trailing zero to achieve the length of 20. ## HID++ Feature Code @@ -134,8 +134,8 @@ following steps: contain the HID++ Feature Code at byte 5 and 6 as `uint16_t` and the Feature Type at byte 7 for a valid Feature Index. In the Feature Type byte, if 7th bit is set this means _Software Hidden_, if bit 8 is set this means - _Obsolete feature_. So, Software_Hidden = (Feature_Type & (1<<6)) and - Obsolete_Feature = (Feature_Type & (1<<7)). + _Obsolete feature_. So, Software_Hidden = (`Feature_Type & (1<<6)`) and + Obsolete_Feature = (`Feature_Type & (1<<7)`). Software Hidden or Obsolete features should not be handled by any application. In case the Feature Index is not valid (i.e.,feature index > number of feature supported) then `0x0200` will be in the response at byte 5 and 6. @@ -170,8 +170,10 @@ Spotlight device can be reset with following HID++ message from the application: In addition to these steps, the Projecteur also pings the device by sending `{0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}` (function code `0x10` and software -identification code `0x0d` in forth byte). The response to this ping contains -the HID++ version supported by the device. +identification code `0x0d` in forth byte; the last byte is a random value that +will returned back on 7th byte in the response message). The response to this +ping contains the HID++ version (`fifth_byte + sixth_byte/10.0`) supported by +the device. Further, Projecteur configures the Logitech device to send `Next Hold` and `Back Hold` events and resets the pointer speed to a default value with @@ -212,7 +214,9 @@ advantageous as it help in implementing Input Mapping feature that official Logitech Software lacks. However, this approach also makes porting Projecteur to different platforms more difficult. -# Wireless Notification on Activation/Deactivation of Spotlight Device +# Important HID++ commands for Spotlight device + +## Wireless Notification on Activation/Deactivation of Spotlight Device The Spotlight device sends a wireless notification if it gets activated. Wireless notification will be short HID++ message if the Spotlight device is @@ -227,20 +231,61 @@ A long HID++ wireless notification is only received for device activation. In this message, third byte will be the Feature Index for the Wireless Notification Feature Code (`0x1db4`). -# Vibration support +## Vibration support The spotlight device can vibrate if the HID++ message `{0x10, 0x01, (Feature Index for Presenter Control Feature Code), 0x1d, length, 0xe8, intensity}` is sent to it. In the message, length can range between `0x00` to `0x0a`. +## Battery Status + +Battery status can be requested by sending request command +`{0x10, 0x01, 0x06, 0x0d, 0x00, 0x00, 0x00}` (assuming the Feature Index for +Battery Status Feature Code (`0x1000`) is `0x06`; function code is `0x00` and +software identification code is `0x0d`). In the response, the fifth byte shows +current battery level in percent, sixth byte shows the next reported battery +level in percent (device do not report continuous battery level) and the seventh +byte shows the state of battery with following possible values. + +``` +enum class BatteryStatus : uint8_t {Discharging = 0x00, + Charging = 0x01, + AlmostFull = 0x02, + Full = 0x03, + SlowCharging = 0x04, + InvalidBattery = 0x05, + ThermalError = 0x06, + ChargingError = 0x07 + }; +``` + # Processing of device response All of the HID++ commands listed above result in response messages from the Spotlight device. For most messages, these responses from device are just the acknowledgements of the HID++ commands sent by the application. However, some -responses from the Spotlight device contain useful information. For details on -processing such responses see the `onHidppDataAvailable` method in the -`Spotlight` class in [spotlight.h](../src/spotlight.h). +responses from the Spotlight device contain useful information. These responses +are processed in the `onHidppDataAvailable` method in the `Spotlight` class +in [spotlight.h](../src/spotlight.h). Description of HID++ messages from device +to reprogrammed keys (`Next Hold` and `Back Hold`) are provided in following +sub-section: + +## Response to `Next Hold` and `Back Hold` keys + +The first HID++ message sent by Spotlight device at the start and end of any +hold event is `{0x11, 0x01, 0x07, 0x00, (button_code), ...followed by zeroes ....}` +(assuming `0x07` is the Feature Index for ReprogramControlsV4 Feature Code (`0x1b04`)). +When the hold event starts, button_code will be `0xda` for start of `Next Hold` +event and `0xdc` for start of `Back Hold` event. When the button is released +the HID++ message received have `0x00` as button_code for both cases. + +During the Hold event, the Spotlight device sends the relative mouse movement +data as HID++ messages. These messages are of form +`{0x11, 0x01, 0x07, 0x10, (mouse data in 4 bytes), ...followed by zeroes ....}` +(assuming `0x07` is the Feature Index for ReprogramControlsV4 Feature Code (`0x1b04`)). +In the four bytes (for mouse data), the second and last bytes are relative `x` +and `y` values. These relative `x` and `y` values are used for Scrolling and +Volume Control Actions in Projecteur. # Further information diff --git a/src/spotlight.cc b/src/spotlight.cc index 15424346..580146fb 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -473,10 +473,11 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) } } else if (eventCode == 0x10) { // mouse move event - // Mouse data is sent as 4 byte information starting at 4th index and ending at 7th. - // out of these 5th byte and 7th byte are x and y relative change, respectively. - // the forth byte show horizonal scroll towards right if rel value is -1 otherwise left scroll (0) - // the sixth byte show vertical scroll towards up if rel value is -1 otherwise down scroll (0) + // Mouse data is sent as 4 byte information starting at 5th byte and ending at 8th. + // out of these 6th byte and 8th bytes are x and y relative change, respectively. + // Not sure about meaning of 5th and 7th bytes. However during testing + // the 5th byte shows horizonal scroll towards right if rel value is -1 otherwise left scroll (0) + // the 7th byte shows vertical scroll towards up if rel value is -1 otherwise down scroll (0) auto byteToRel = [](int i){return ( (i<128) ? i : 256-i);}; // convert the byte to relative motion in x or y int x = byteToRel(readVal.at(5)); int y = byteToRel(readVal.at(7)); From 705aa34c262aaa34bda2e683c99e8e2b080601b0 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Fri, 13 Aug 2021 11:25:51 +0200 Subject: [PATCH 058/110] Enable ci-builds for pull requests to feature branches. --- .github/workflows/ci-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 9a7cbff5..5e211b47 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -8,6 +8,7 @@ on: branches: - master - develop + - 'feature/**' jobs: build: From 33c83d12997dbf404054b38171aee31710b7b5dc Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Fri, 13 Aug 2021 13:45:08 +0530 Subject: [PATCH 059/110] Improvement to repeated actions Now, repeated actions are time-gapped resulting better behavior in general. Volume Control action do not use `amixer`. Rather it emits the volume Up/Down input events. --- src/deviceinput.cc | 10 +++++++++- src/deviceinput.h | 2 ++ src/spotlight.cc | 43 ++++++++++++++++++++++++++++--------------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 3ea4a6a9..204d3b8b 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -508,7 +508,12 @@ InputMapper::Impl::Impl(InputMapper* parent, std::shared_ptr vdev // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::execAction(const std::shared_ptr& action, DeviceKeyMap::Result r) { - if (!action) return; + if (!action || action->empty()) return; + if (action->isRepeated()) + { + if(m_parent->m_repeatedActionTimer->isActive()) return; + m_parent->m_repeatedActionTimer->start(); + } logDebug(input) << "Input map action, type = " << int(action->type()) << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit); @@ -567,7 +572,10 @@ void InputMapper::Impl::record(const struct input_event input_events[], size_t n InputMapper::InputMapper(std::shared_ptr virtualDevice, QObject* parent) : QObject(parent) , impl(std::make_unique(this, std::move(virtualDevice))) + , m_repeatedActionTimer(new QTimer(this)) { + m_repeatedActionTimer->setSingleShot(true); + m_repeatedActionTimer->setInterval(25); // time gap between repeated action } // ------------------------------------------------------------------------------------------------- diff --git a/src/deviceinput.h b/src/deviceinput.h index 911e6910..5da33264 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -11,6 +11,7 @@ #include class VirtualDevice; +class QTimer; // ------------------------------------------------------------------------------------------------- /// This is basically the input_event struct from linux/input.h without the time member @@ -340,4 +341,5 @@ class InputMapper : public QObject struct Impl; std::unique_ptr impl; ReservedInput reservedInputs; + QTimer* m_repeatedActionTimer = nullptr; // Timer for introducing time gap between repeated ations }; diff --git a/src/spotlight.cc b/src/spotlight.cc index 15424346..bc81cde4 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -199,7 +199,6 @@ int Spotlight::connectDevices() { if (!(action->isRepeated()) && m_holdButtonStatus.numEvents() > 0) return; - static auto sign = [](int i) { return i/abs(i); }; auto emitNativeKeySequence = [this](const NativeKeySequence& ks) { if (!m_virtualDevice) return; @@ -218,9 +217,14 @@ int Spotlight::connectDevices() if (action->type() == Action::Type::KeySequence) { + if (!m_virtualDevice) return; + const auto keySequenceAction = static_cast(action.get()); - logInfo(input) << "Emitting Key Sequence:" << keySequenceAction->keySequence.toString(); - emitNativeKeySequence(keySequenceAction->keySequence); + if (!keySequenceAction->keySequence.empty()) + { + logDebug(input) << "Emitting Key Sequence:" << keySequenceAction->keySequence.toString(); + emitNativeKeySequence(keySequenceAction->keySequence); + } } if (action->type() == Action::Type::CyclePresets) @@ -247,23 +251,24 @@ int Spotlight::connectDevices() if (!m_virtualDevice) return; int param = 0; - uint16_t wheelType = (action->type() == Action::Type::ScrollHorizontal) ? REL_HWHEEL : REL_WHEEL; if (action->type() == Action::Type::ScrollHorizontal) param = static_cast(action.get())->param; if (action->type() == Action::Type::ScrollVertical) param = static_cast(action.get())->param; - if (param) - for (int j=0; jemitEvents({{{},EV_REL, wheelType, sign(param)}}); + uint16_t wheelCode = (action->type() == Action::Type::ScrollHorizontal) ? REL_HWHEEL : REL_WHEEL; + const std::vector scrollInputEvents = {{{}, EV_REL, wheelCode, param}, {{}, EV_SYN, SYN_REPORT, 0},}; + + if (param) m_virtualDevice->emitEvents(scrollInputEvents); } if (action->type() == Action::Type::VolumeControl) { - auto param = static_cast(action.get())->param; - if (param) QProcess::execute("amixer", - QStringList({"set", "Master", - tr("%1\%%2").arg(abs(param)).arg(sign(param)==1?"+":"-"), - "-q"})); + if (!m_virtualDevice) return; + auto param = static_cast(action.get())->param; + uint16_t keyCode = (param > 0)? KEY_VOLUMEUP: KEY_VOLUMEDOWN; + const std::vector curVolInputEvents = {{{}, EV_KEY, keyCode, abs(param)}, {{}, EV_SYN, SYN_REPORT, 0}, + {{}, EV_KEY, keyCode, 0}, {{}, EV_SYN, SYN_REPORT, 0},}; + if (param) m_virtualDevice->emitEvents(curVolInputEvents); } }); @@ -484,20 +489,28 @@ void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) if (action && !action->empty()) { + auto getReducedParam = [](int param, int limit=2){ // reduce the values from Spotlight device for better scroll behavior + int minVal=5; + if (abs(param) < minVal) return 0; // ignore small device movement + + auto sign = (param == 0)? 0: ((param > 0)? 1:-1); + return ((abs(param) > minVal*limit)? sign*minVal*limit : param)/minVal; // limit return value between -limit to limit + }; + if (action->type() == Action::Type::ScrollHorizontal) { const auto scrollHAction = static_cast(action.get()); - scrollHAction->param = -(abs(x) > 60? 60 : x)/20; // reduce the values from Spotlight device + scrollHAction->param = -(getReducedParam(x)); } if (action->type() == Action::Type::ScrollVertical) { const auto scrollVAction = static_cast(action.get()); - scrollVAction->param = (abs(y) > 60? 60 : y)/20; // reduce the values from Spotlight device + scrollVAction->param = getReducedParam(y); } if(action->type() == Action::Type::VolumeControl) { const auto volumeControlAction = static_cast(action.get()); - volumeControlAction->param = -y/20; // reduce the values from Spotlight device + volumeControlAction->param = -getReducedParam(y, 3); } // feed the keystroke to InputMapper and let it trigger the associated action From 05db0a2f2ec0fa9db0f73461034bef1af30486b8 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 13 Aug 2021 16:05:43 +0200 Subject: [PATCH 060/110] Skip certain build steps if current repo != projecteur main repo. --- .github/workflows/ci-build.yml | 7 ++++--- .github/workflows/codeql-analysis.yml | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5e211b47..77c7d8aa 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -142,14 +142,14 @@ jobs: echo "REPO_UPLOAD=${{ false }}" >> $GITHUB_ENV - name: Check for binary-pkg upload conditions - if: env.BRANCH == 'develop' || env.BRANCH == 'master' + if: ${{ (env.BRANCH == 'develop' || env.BRANCH == 'master') && github.repo == 'jahnf/Projecteur' }} run: | echo "upload_bin_pkg=${{ true }}" >> $GITHUB_ENV pip install --upgrade wheel pip install --upgrade cloudsmith-cli - name: Check for source-pkg upload conditions - if: env.upload_bin_pkg == 'true' && startsWith(matrix.docker_tag, 'archlinux') + if: ${{ env.upload_bin_pkg == 'true' && startsWith(matrix.docker_tag, 'archlinux') && github.repo == 'jahnf/Projecteur' }} run: | echo "upload_src_pkg=${{ true }}" >> $GITHUB_ENV @@ -170,7 +170,7 @@ jobs: --summary "${CLOUDSMITH_SUMMARY}" --description "${CLOUDSMITH_DESC}" ${{ env.dist_pkg_artifact }} - name: Upload raw source-pkg to cloudsmith - if: env.upload_src_pkg == 'true' + if: ${{ env.upload_src_pkg == 'true' && github.repo == 'jahnf/Projecteur' }} env: CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} CLOUDSMITH_REPO: ${{ env.cloudsmith_upload_repo }} @@ -214,6 +214,7 @@ jobs: # ===================================================================================== # ---------- Upload artifacts to projecteur server ------------ projecteur-bin-upload: + if: ${{ github.repo == 'jahnf/Projecteur' }} needs: build runs-on: ubuntu-latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 27414be4..3b0a9358 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,16 +9,17 @@ on: jobs: analyse: + if: ${{ github.repo == 'jahnf/Projecteur' }} name: Analyse runs-on: ubuntu-20.04 steps: - name: Install dependencies - run: | + run: | sudo apt-get --no-install-recommends install pkg-config qtdeclarative5-dev \ qttools5-dev-tools qttools5-dev \ qt5-default libqt5x11extras5-dev - + - name: Checkout repository uses: actions/checkout@v2 with: @@ -41,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 - with: + with: queries: +security-and-quality - name: Build project From bb285782605f560c211e1374ce3747abeb6ce47d Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 13 Aug 2021 16:25:47 +0200 Subject: [PATCH 061/110] Fix workflow variable name. --- .github/workflows/ci-build.yml | 8 ++++---- .github/workflows/codeql-analysis.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 77c7d8aa..685f9fc6 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -142,14 +142,14 @@ jobs: echo "REPO_UPLOAD=${{ false }}" >> $GITHUB_ENV - name: Check for binary-pkg upload conditions - if: ${{ (env.BRANCH == 'develop' || env.BRANCH == 'master') && github.repo == 'jahnf/Projecteur' }} + if: ${{ (env.BRANCH == 'develop' || env.BRANCH == 'master') && github.repository == 'jahnf/Projecteur' }} run: | echo "upload_bin_pkg=${{ true }}" >> $GITHUB_ENV pip install --upgrade wheel pip install --upgrade cloudsmith-cli - name: Check for source-pkg upload conditions - if: ${{ env.upload_bin_pkg == 'true' && startsWith(matrix.docker_tag, 'archlinux') && github.repo == 'jahnf/Projecteur' }} + if: ${{ env.upload_bin_pkg == 'true' && startsWith(matrix.docker_tag, 'archlinux') && github.repository == 'jahnf/Projecteur' }} run: | echo "upload_src_pkg=${{ true }}" >> $GITHUB_ENV @@ -170,7 +170,7 @@ jobs: --summary "${CLOUDSMITH_SUMMARY}" --description "${CLOUDSMITH_DESC}" ${{ env.dist_pkg_artifact }} - name: Upload raw source-pkg to cloudsmith - if: ${{ env.upload_src_pkg == 'true' && github.repo == 'jahnf/Projecteur' }} + if: ${{ env.upload_src_pkg == 'true' && github.repository == 'jahnf/Projecteur' }} env: CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} CLOUDSMITH_REPO: ${{ env.cloudsmith_upload_repo }} @@ -214,7 +214,7 @@ jobs: # ===================================================================================== # ---------- Upload artifacts to projecteur server ------------ projecteur-bin-upload: - if: ${{ github.repo == 'jahnf/Projecteur' }} + if: ${{ github.repository == 'jahnf/Projecteur' }} needs: build runs-on: ubuntu-latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3b0a9358..bf734bf8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,7 +9,7 @@ on: jobs: analyse: - if: ${{ github.repo == 'jahnf/Projecteur' }} + if: ${{ github.repository == 'jahnf/Projecteur' }} name: Analyse runs-on: ubuntu-20.04 From 99a6f420d1ebf384b143cc24580e65b6da447a81 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 13 Aug 2021 18:14:46 +0200 Subject: [PATCH 062/110] Refactoring of hid++ functionality part 1. --- .clang-format | 34 ++- CMakeLists.txt | 1 + doc/CHANGELOG.md | 1 + src/aboutdlg.cc | 6 +- src/device-defs.h | 13 + src/device-hidpp.cc | 614 ++++++++++++++++++++++++++++++++++++++++++ src/device-hidpp.h | 83 ++++++ src/device.cc | 458 +++++++------------------------ src/device.h | 135 +++------- src/deviceinput.cc | 83 +++--- src/deviceinput.h | 2 - src/devicescan.h | 29 +- src/enum-helper.h | 26 +- src/hidpp.cc | 587 +++++++++++++++++++++++++++++----------- src/hidpp.h | 280 +++++++++++++++---- src/iconwidgets.h | 2 + src/inputmapconfig.cc | 1 - src/inputmapconfig.h | 4 + src/main.cc | 10 + src/projecteurapp.h | 1 + src/spotlight.cc | 365 ++++++++++++------------- src/spotlight.h | 6 +- 22 files changed, 1833 insertions(+), 908 deletions(-) create mode 100644 src/device-defs.h create mode 100644 src/device-hidpp.cc create mode 100644 src/device-hidpp.h diff --git a/.clang-format b/.clang-format index f56f280b..27021c78 100644 --- a/.clang-format +++ b/.clang-format @@ -1,9 +1,26 @@ --- BasedOnStyle: LLVM +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +MaxEmptyLinesToKeep: 2 +ColumnLimit: 100 + Language: Cpp +#LambdaBodyIndentation: OuterScope +Cpp11BracedListStyle: true +PointerAlignment: Left +ConstructorInitializerIndentWidth: '2' +ContinuationIndentWidth: 2 +SortIncludes: 'true' +EmptyLineBeforeAccessModifier: Leave +BinPackArguments: 'true' +BinPackParameters: 'true' AlignAfterOpenBracket: Align AlignEscapedNewlines: Left +KeepEmptyLinesAtTheStartOfBlocks: true AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true AlwaysBreakTemplateDeclarations: 'Yes' @@ -11,23 +28,20 @@ AllowShortFunctionsOnASingleLine: Inline AllowShortBlocksOnASingleLine: Always AllowShortEnumsOnASingleLine: true BreakConstructorInitializers: BeforeComma -ColumnLimit: '100' -ConstructorInitializerIndentWidth: '2' -IndentWidth: '2' -MaxEmptyLinesToKeep: '2' -PointerAlignment: Left -SortIncludes: 'true' -Standard: Auto -TabWidth: '2' -UseTab: Never +BreakBeforeConceptDeclarations: 'true' +Standard: c++14 +EmptyLineBeforeAccessModifier: Always BreakBeforeBinaryOperators: NonAssignment AlignConsecutiveAssignments: true NamespaceIndentation: Inner BreakBeforeBraces: Custom BraceWrapping: - BeforeLambdaBody: false + AfterClass: true AfterControlStatement: MultiLine SplitEmptyFunction: false SplitEmptyRecord: false + BeforeElse: true + BeforeLambdaBody: false + ... diff --git a/CMakeLists.txt b/CMakeLists.txt index 358b2aa0..eb5aeaf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ add_executable(projecteur src/actiondelegate.cc src/actiondelegate.h src/colorselector.cc src/colorselector.h src/device.cc src/device.h + src/device-hidpp.cc src/device-hidpp.h src/device-vibration.cc src/device-vibration.h src/deviceinput.cc src/deviceinput.h src/devicescan.cc src/devicescan.h diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 539564a8..db23d076 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -9,6 +9,7 @@ - Bug fix for high CPU load in certain situations ([#133][i133]) - Bug fix for wrong button mapping for inputs with same length ([#144][i144]) - Added automated builds for Fedora 34, Debian 11 (Bullseye) and OpenSUSE 15.3 ([#148][i148]) +- Bug fix for crash when closing the about dialog. Many thanks to *[@mayanksuman][c-mayanksuman]* for Logitech Bluetooth, Scrolling and Audio volume support. diff --git a/src/aboutdlg.cc b/src/aboutdlg.cc index 4e315834..64ae1654 100644 --- a/src/aboutdlg.cc +++ b/src/aboutdlg.cc @@ -123,8 +123,10 @@ QWidget* AboutDialog::createVersionInfoWidget() tr("Version %1", "%1=application version number") .arg(projecteur::version_string())), this); vbox->addWidget(versionLabel); - const auto vInfo = QString("git-branch: %1
git-hash: %2") - .arg(projecteur::version_branch(), projecteur::version_shorthash()); + const auto vInfo = QString("git-branch: %1
git-hash: %2
build-type: %3") + .arg(projecteur::version_branch(), + projecteur::version_shorthash(), + projecteur::version_buildtype()); versionLabel->setToolTip(vInfo); if (QString(projecteur::version_flag()).size() || diff --git a/src/device-defs.h b/src/device-defs.h new file mode 100644 index 00000000..ba9526d2 --- /dev/null +++ b/src/device-defs.h @@ -0,0 +1,13 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + +#pragma once + +#include + +// Bus on which device is connected +enum class BusType : uint8_t { Unknown, Usb, Bluetooth }; + +enum class ConnectionType : uint8_t { Event, Hidraw }; + +enum class ConnectionMode : uint8_t { ReadOnly, WriteOnly, ReadWrite }; diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc new file mode 100644 index 00000000..d823e072 --- /dev/null +++ b/src/device-hidpp.cc @@ -0,0 +1,614 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + +#include "device-hidpp.h" +#include "logging.h" + +#include + +#include "deviceinput.h" + +#include +#include + +DECLARE_LOGGING_CATEGORY(hid) + +// ------------------------------------------------------------------------------------------------- +SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token, + const DeviceScan::SubDevice& sd, const DeviceId& id) + : SubHidrawConnection(token, sd) + , m_featureSet(this) + , m_busType(id.busType) + , m_requestCleanupTimer(new QTimer(this)) +{ + m_requestCleanupTimer->setInterval(500); + m_requestCleanupTimer->setSingleShot(false); + connect(m_requestCleanupTimer, &QTimer::timeout, this, &SubHidppConnection::clearTimedOutRequests); +} + +// ------------------------------------------------------------------------------------------------- +SubHidppConnection::~SubHidppConnection() = default; + +// ------------------------------------------------------------------------------------------------- +ssize_t SubHidppConnection::sendData(std::vector data) { + return sendData(HIDPP::Message(std::move(data))); +} + +// ------------------------------------------------------------------------------------------------- +ssize_t SubHidppConnection::sendData(HIDPP::Message msg) // +{ + constexpr ssize_t errorResult = -1; + if (!msg.isValid()) { + return errorResult; + } + + // If the message have 0xff as second byte, it is meant for USB dongle hence, + // should not be send when device is connected on bluetooth. + // + // Logitech Spotlight (USB) can receive data in two different length. + // 1. Short (7 byte long starting with 0x10) + // 2. Long (20 byte long starting with 0x11) + // However, bluetooth connection only accepts data in long (20 byte) packets. + // For converting standard short length data to long length data, change the first byte to 0x11 + // and pad the end of message with 0x00 to acheive the length of 20. + + if (m_busType == BusType::Bluetooth) + { + if (msg.deviceIndex() == HIDPP::DeviceIndex::DefaultDevice) { + logWarn(hid) << "Invalid packet" << msg.hex() << "for spotlight connected on bluetooth."; + return errorResult; + } + + // For bluetooth always convert to a long message if we have a short message + msg.convertToLong(); + } + + return SubHidrawConnection::sendData(msg.data(), msg.size()); +} + + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendData(std::vector data, SendResultCallback resultCb) { + sendData(HIDPP::Message(std::move(data)), std::move(resultCb)); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendData(HIDPP::Message msg, SendResultCallback resultCb) { + postSelf([this, msg = std::move(msg), cb = std::move(resultCb)]() mutable { + // Check for valid message format + if (!msg.isValid()) { + if (cb) cb(MsgResult::InvalidFormat); + return; + } + + if (m_busType == BusType::Bluetooth) { + // For bluetooth always convert to a long message if we have a short message + msg.convertToLong(); + } + + const auto result = SubHidrawConnection::sendData(msg.data(), msg.size()); + if (cb) { + const bool success = (result >= 0 && static_cast(result) == msg.size()); + cb(success ? MsgResult::Ok : MsgResult::WriteError); + } + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendRequest(std::vector data, RequestResultCallback responseCb) { + sendRequest(HIDPP::Message(std::move(data)), std::move(responseCb)); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback responseCb) +{ + postSelf([this, msg = std::move(msg), cb = std::move(responseCb)]() mutable + { + // Check for valid message format + if (!msg.isValid()) { + if (cb) cb(MsgResult::InvalidFormat, HIDPP::Message()); + return; + } + + if (m_busType == BusType::Bluetooth) { + // For bluetooth always convert to a long message if we have a short message + msg.convertToLong(); + } + + // TODO: more early sanity checks?? device index in a valid range??? + + sendData(msg, [this, msg](MsgResult result) { + if (result == MsgResult::Ok) return; + + // error result, find msg in request list + auto it = std::find_if(m_requests.begin(), m_requests.end(), + [&msg](const RequestEntry& entry) { return entry.request == msg; }); + + if (it == m_requests.end()) { + // TODO log warning send error for message without matching request + return; + } + + if (it->callBack) { + it->callBack(result, HIDPP::Message()); + } + m_requests.erase(it); + }); + + // Place request in request list with a timeout + m_requests.emplace_back(RequestEntry{ + std::move(msg), std::chrono::steady_clock::now() + std::chrono::milliseconds{4000}, + std::move(cb)}); + + // Run cleanup timer if not already active + if (!m_requestCleanupTimer->isActive()) m_requestCleanupTimer->start(); + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, + bool continueOnError) { + std::vector results; + results.reserve(dataBatch.size()); + sendDataBatch(std::move(dataBatch), std::move(cb), continueOnError, std::move(results)); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, + bool continueOnError, std::vector results) +{ + postSelf([this, batch = std::move(dataBatch), batchCb = std::move(cb), continueOnError, + results = std::move(results), coe = continueOnError]() mutable + { + if (batch.empty()) { + if (batchCb) batchCb(std::move(results)); + return; + } + + // Get item from queue and pop + DataBatchItem queueItem(std::move(batch.front())); + batch.pop(); + + // Process queue item + sendData(std::move(queueItem.message), makeSafeCallback( + [this, batch = std::move(batch), results = std::move(results), coe, + batchCb = std::move(batchCb), resultCb = std::move(queueItem.callback)] + (MsgResult result) mutable + { + // Add result to results vector + results.push_back(result); + // If a result callback is set invoke it + if (resultCb) resultCb(result); + + // If batch is empty or we got an error result and don't want to continue on + // error (coe) + if (batch.empty() || (result != MsgResult::Ok && !coe)) { + if (batchCb) batchCb(std::move(results)); + return; + } + + // continue processing the rest of the batch + sendDataBatch(std::move(batch), std::move(batchCb), coe, std::move(results)); + })); + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, + bool continueOnError) { + std::vector results; + results.reserve(requestBatch.size()); + sendRequestBatch(std::move(requestBatch), std::move(cb), continueOnError, std::move(results)); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, + bool continueOnError, std::vector results) +{ + postSelf([this, batch = std::move(requestBatch), batchCb = std::move(cb), continueOnError, + results = std::move(results), coe = continueOnError]() mutable + { + if (batch.empty()) { + if (batchCb) batchCb(std::move(results)); + return; + } + + // Get item from queue and pop + RequestBatchItem queueItem(std::move(batch.front())); + batch.pop(); + + // Process queue item + sendRequest(std::move(queueItem.message), makeSafeCallback( + [this, batch = std::move(batch), results = std::move(results), coe, + batchCb = std::move(batchCb), resultCb = std::move(queueItem.callback)] + (MsgResult result, HIDPP::Message replyMessage) mutable + { + // Add result to results vector + results.push_back(result); + // If a result callback is set invoke it + if (resultCb) resultCb(result, std::move(replyMessage)); + + // If batch is empty or we got an error result and don't want to continue on + // error (coe) + if (batch.empty() || (result != MsgResult::Ok && !coe)) { + if (batchCb) batchCb(std::move(results)); + return; + } + + // continue processing the rest of the batch + sendRequestBatch(std::move(batch), std::move(batchCb), coe, std::move(results)); + })); + }); +} + +// ------------------------------------------------------------------------------------------------- +std::shared_ptr SubHidppConnection::create(const DeviceScan::SubDevice& sd, + const DeviceConnection& dc) { + const int devfd = openHidrawSubDevice(sd, dc.deviceId()); + if (devfd == -1) return std::shared_ptr(); + + auto connection = std::make_shared(Token{}, sd, dc.deviceId()); + if (dc.hasHidppSupport()) connection->m_details.deviceFlags |= DeviceFlag::Hidpp; + + connection->createSocketNotifiers(devfd); + connection->m_inputMapper = dc.inputMapper(); + + connect(connection->socketReadNotifier(), &QSocketNotifier::activated, &*connection, + &SubHidppConnection::onHidppDataAvailable); + + connection->postTask([c = &*connection]() { c->initialize(); }); + return connection; +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) { + if (!hasFlags(DeviceFlags::Vibrate)) return; + + // TODO put in HIDPP + // TODO generalize features and protocol for proprietary device features like vibration + // for not only the Spotlight device. + // + // Spotlight: + // present + // controlID len intensity + // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; + + length = length > 10 ? 10 : length; // length should be between 0 to 10. + const uint8_t pcIndex = m_featureSet.getFeatureIndex(HIDPP::FeatureCode::PresenterControl); + using namespace HIDPP; + // const uint8_t vibrateCmd[] = {HIDPP::Bytes::SHORT_MSG, + // HIDPP::Bytes::MSG_TO_SPOTLIGHT, + // pcIndex, + // m_featureSet.getRandomFunctionCode(0x10), + // length, + // 0xe8, + // intensity}; + Message vibrateMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, pcIndex, 1, { + length, 0xe8, intensity + }); + if (pcIndex) sendData(std::move(vibrateMsg)); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::queryBatteryStatus() +{ + // TODO put parts in HIDPP + // if (hasFlags(DeviceFlag::ReportBattery)) { + // const uint8_t batteryFeatureIndex = + // m_featureSet.getFeatureIndex(HIDPP::FeatureCode::BatteryStatus); + // if (batteryFeatureIndex) { + // const uint8_t batteryCmd[] = {HIDPP::Bytes::SHORT_MSG, + // HIDPP::Bytes::MSG_TO_SPOTLIGHT, + // batteryFeatureIndex, + // m_featureSet.getRandomFunctionCode(0x00), + // 0x00, + // 0x00, + // 0x00}; + // sendData(batteryCmd, sizeof(batteryCmd)); + // } + // } +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::setPointerSpeed(uint8_t +//level +) +{ + const uint8_t psIndex = m_featureSet.getFeatureIndex(HIDPP::FeatureCode::PointerSpeed); + if (psIndex == 0x00) return; + + // level = (level > 0x09) ? 0x09 : level; // level should be in range of 0-9 + // uint8_t pointerSpeed = 0x10 & level; // pointer speed sent to device are between 0x10 - 0x19 (hence ten speed levels) + // const uint8_t pointerSpeedCmd[] = {HIDPP::Bytes::SHORT_MSG, + // HIDPP::Bytes::MSG_TO_SPOTLIGHT, + // psIndex, + // m_featureSet.getRandomFunctionCode(0x10), + // pointerSpeed, + // 0x00, + // 0x00}; + // sendData(pointerSpeedCmd, sizeof(pointerSpeedCmd)); +} + +// ------------------------------------------------------------------------------------------------- + +void SubHidppConnection::initUsbReceiver(std::function cb) +{ + if (m_busType != BusType::Usb) + { + if (cb) cb(MsgResult::Ok); + return; + } + + using namespace HIDPP; + using Type = HIDPP::Message::Type; + RequestBatch batch{{ + RequestBatchItem{ + // Reset device: get rid of any device configuration by other programs + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 0, {}), + makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); + }) + }, + RequestBatchItem{ + // Turn off software bit and keep the wireless notification bit on + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x01, 0x00}), + makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); + }) + }, + RequestBatchItem{ + // Initialize USB dongle + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 2, {}), + makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); + }) + }, + RequestBatchItem{ + // --- + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 2, {0x02, 0x00, 0x00}), + makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); + }) + }, + RequestBatchItem{ + // Now enable both software and wireless notification bit + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x09, 0x00}), + makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); + }) + }, + }}; + + sendRequestBatch(std::move(batch), [cb=std::move(cb)](std::vector results) { + if (cb) cb(results.back()); + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::initialize() +{ + if (!hasFlags(DeviceFlag::Hidpp)) return; + + // TODO set state: not_initialized, initializing, initialized for the subdevice + + initUsbReceiver(makeSafeCallback([this](MsgResult res) + { + if (res != MsgResult::Ok) { + // TODO log error - re-schedule init? + } + m_featureSet.initFromDevice(); + })); + + // DeviceFlags featureFlags = DeviceFlag::NoFlags; + // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from logitech device + // setNotifiersEnabled(false); + // setReadNotifierEnabled(false); // TODO remove ... implement populatefeaturetable via async.. + // if (m_featureSet.getFeatureCount() == 0) m_featureSet.populateFeatureTable(); + // if (m_featureSet.getFeatureCount()) { + // logDebug(hid) << "Loaded" << m_featureSet.getFeatureCount() << "features for" << path(); + // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::PresenterControl)) { + // featureFlags |= DeviceFlag::Vibrate; + // logDebug(hid) << "SubDevice" << path() << "reported Vibration capabilities."; + // } + // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::BatteryStatus)) { + // featureFlags |= DeviceFlag::ReportBattery; + // logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; + // } + // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::ReprogramControlsV4)) { + // auto& reservedInputs = m_inputMapper->getReservedInputs(); + // reservedInputs.clear(); + // featureFlags |= DeviceFlags::NextHold; + // featureFlags |= DeviceFlags::BackHold; + // reservedInputs.emplace_back(ReservedKeyEventSequence::NextHoldInfo); + // reservedInputs.emplace_back(ReservedKeyEventSequence::BackHoldInfo); + // logDebug(hid) << "SubDevice" << path() << "can send next and back hold event."; + // } + // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::PointerSpeed)) { + // featureFlags |= DeviceFlags::PointerSpeed; + // } + // } + // else { + // logWarn(hid) << "Loading FeatureSet for" << path() << "failed."; + // logInfo(hid) << "Device might be inactive. Press any button on device to activate it."; + // } + // setFlags(featureFlags, true); + // setReadNotifierEnabled(true); + + // TODO: implement + // // Reset spotlight device + // if (m_featureSet.getFeatureCount()) { + // const auto resetIndex = m_featureSet.getFeatureIndex(FeatureCode::Reset); + // if (resetIndex) { + // QTimer::singleShot(delay_ms*msgCount, this, [this, resetIndex](){ + // const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, + // resetIndex, m_featureSet.getRandomFunctionCode(0x10), 0x00, 0x00, 0x00}; sendData(data, + // sizeof(data));}); + // msgCount++; + // } + // } + // Device Resetting complete ------------------------------------------------- + + // TODO: implement + // if (m_busType == BusType::Usb) { + // // Ping spotlight device for checking if is online + // // the response will have the version for HID++ protocol. + // QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); + // msgCount++; + // } else if (m_busType == BusType::Bluetooth) { + // // Bluetooth connection do not respond to ping. + // // Hence, we are faking a ping response here. + // // Bluetooth connection mean HID++ v2.0+. + // // Setting version to 6.4: same as USB connection. + // setHIDppProtocol(6.4); + // } + + // TODO implement + // Enable Next and back button on hold functionality. + const auto rcIndex = m_featureSet.getFeatureIndex(HIDPP::FeatureCode::ReprogramControlsV4); + if (rcIndex) { + // if (hasFlags(DeviceFlags::NextHold)) { + // QTimer::singleShot(delay_ms*msgCount, this, [this, rcIndex](){ + // const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, + // rcIndex, m_featureSet.getRandomFunctionCode(0x30), 0x00, 0xda, 0x33, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x00}; + // sendData(data, sizeof(data));}); + // msgCount++; + // } + + // if (hasFlags(DeviceFlags::BackHold)) { + // QTimer::singleShot(delay_ms*7777777msgCount, this, [this, rcIndex](){ + // const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, + // rcIndex, m_featureSet.getRandomFunctionCode(0x30), 0x00, 0xdc, 0x33, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00, 0x00}; + // sendData(data, sizeof(data));}); + // msgCount++; + // } + } + + // Reset pointer speed to default level of 0x04 (5th level) + if (hasFlags(DeviceFlags::PointerSpeed)) setPointerSpeed(0x04); + + + // m_featureSet.initFromDevice(); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::pingSubDevice() { + // constexpr uint8_t rootIndex = 0x00; // root Index is always 0x00 in any logitech device + // const uint8_t pingCmd[] = {HIDPP::Bytes::SHORT_MSG, + // HIDPP::Bytes::MSG_TO_SPOTLIGHT, + // rootIndex, + // m_featureSet.getRandomFunctionCode(0x10), + // 0x00, + // 0x00, + // 0x5d}; + // sendData(pingCmd, sizeof(pingCmd)); +} + +// // ------------------------------------------------------------------------------------------------- +// void SubHidppConnection::setHIDppProtocol(float version) { +// // Inform user about the online status of device. +// if (version > 0) { +// if (HIDppProtocolVer < 0) { +// logDebug(hid) << "HID++ Device with path" << path() << "is now active."; +// logDebug(hid) << "HID++ protocol version" << tr("%1.").arg(version); +// emit activated(); +// } +// } +// else { +// if (HIDppProtocolVer > 0) { +// logDebug(hid) << "HID++ Device with path" << path() << "got deactivated."; +// emit deactivated(); +// } +// } +// HIDppProtocolVer = version; +// } + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::onHidppDataAvailable(int fd) +{ + HIDPP::Message msg(std::vector(20)); + const auto res = ::read(fd, msg.data(), msg.dataSize()); + if (res < 0) { + if (errno != EAGAIN) { + emit socketReadError(errno); + } + return; + } + + if (!msg.isValid()) { + logDebug(hid) << tr("Received invalid HID++ message " + "'%1' from %2").arg(qPrintable(msg.hex()), path()); + return; + } + + if (msg.isError()) { + // Find first matching request for the incoming error reply + const auto it = + std::find_if(m_requests.begin(), m_requests.end(), [&msg](const RequestEntry& requestEntry) { + return msg.isErrorResponseTo(requestEntry.request); + }); + + if (it != m_requests.end()) + { + logDebug(hid) << tr("Received hiddpp error with code = %1 on") + .arg(to_integral(msg.errorCode())) << path() << "(" << msg.hex() << ")"; + if (it->callBack) { + it->callBack(MsgResult::HidppError, std::move(msg)); + } + m_requests.erase(it); + } + else { + logWarn(hid) << tr("Received error hidpp message '%1' " + "without matching request.").arg(qPrintable(msg.hex())); + } + return; + } + + // Find first matching request for the incoming reply + const auto it = + std::find_if(m_requests.begin(), m_requests.end(), [&msg](const RequestEntry& requestEntry) { + return msg.isResponseTo(requestEntry.request); + }); + + if (it != m_requests.end()) + { + // Found matching request + logDebug(hid) << tr("Received %1 bytes on").arg(msg.size()) << path() + << "(" << msg.hex() << ")"; + if (it->callBack) { + it->callBack(MsgResult::Ok, std::move(msg)); + } + m_requests.erase(it); + } + else { + // TODO check for device event messages, that don't require a request + logWarn(hid) << tr("Received hidpp message " + "'%1' without matching request.").arg(qPrintable(msg.hex())); + } +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::clearTimedOutRequests() { + const auto now = std::chrono::steady_clock::now(); + m_requests.remove_if([&now](const RequestEntry& entry) { + if (now <= entry.validUntil) { + return false; + } + if (entry.callBack) { + entry.callBack(MsgResult::Timeout, HIDPP::Message()); + } + return true; + }); + + if (m_requests.empty()) { + m_requestCleanupTimer->stop(); + } +} diff --git a/src/device-hidpp.h b/src/device-hidpp.h new file mode 100644 index 00000000..92f600b8 --- /dev/null +++ b/src/device-hidpp.h @@ -0,0 +1,83 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md +#pragma once + +#include "device.h" +#include "hidpp.h" + +#include +#include + +class QTimer; + +// ------------------------------------------------------------------------------------------------- +/// @brief TODO +class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInterface +{ + Q_OBJECT + +public: + enum class State : uint8_t {Uninitialized, Initializing, Initialized, Error}; + + static std::shared_ptr create(const DeviceScan::SubDevice& sd, + const DeviceConnection& dc); + + SubHidppConnection(SubHidrawConnection::Token, const DeviceScan::SubDevice& sd, + const DeviceId& id); + ~SubHidppConnection(); + + using SubHidrawConnection::sendData; + + // --- HidppConnectionInterface implementation: + + BusType busType() const override { return m_busType; } + ssize_t sendData(std::vector msg) override; + ssize_t sendData(HIDPP::Message msg) override; + void sendData(std::vector msg, SendResultCallback resultCb) override; + void sendData(HIDPP::Message msg, SendResultCallback resultCb) override; + void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, + bool continueOnError = false) override; + void sendRequest(std::vector msg, RequestResultCallback responseCb) override; + void sendRequest(HIDPP::Message msg, RequestResultCallback responseCb) override; + void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, + bool continueOnError = false) override; + + // ------- + + void queryBatteryStatus() override; + void sendVibrateCommand(uint8_t intensity, uint8_t length) override; + void pingSubDevice(); + void setPointerSpeed(uint8_t level); + // void setHIDppProtocol(float version); + // float getHIDppProtocol() const override { return HIDppProtocolVer; }; + // bool isOnline() const override { return (HIDppProtocolVer > 0); }; + + void initialize(); + +signals: + void receivedBatteryInfo(QByteArray batteryData); + void activated(); + void deactivated(); + +private: + void onHidppDataAvailable(int fd); + void clearTimedOutRequests(); + void initUsbReceiver(std::function); + + void sendDataBatch(DataBatch requestBatch, DataBatchResultCallback cb, bool continueOnError, + std::vector results); + void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, + bool continueOnError, std::vector results); + + HIDPP::FeatureSet m_featureSet; + const BusType m_busType = BusType::Unknown; + + struct RequestEntry { + HIDPP::Message request; // bytes 0(or 1) to 5 should be enough to check against reply + std::chrono::time_point validUntil; + RequestResultCallback callBack; + }; + + std::list m_requests; + QTimer* m_requestCleanupTimer = nullptr; +}; diff --git a/src/device.cc b/src/device.cc index 2ad66a55..45b210fc 100644 --- a/src/device.cc +++ b/src/device.cc @@ -21,61 +21,7 @@ namespace { static const auto registeredComparator_ = QMetaType::registerComparators(); const auto hexId = logging::hexId; - class i18n : public QObject {}; // for i18n and logging - - // ----------------------------------------------------------------------------------------------- - /// Open a hidraw subdevice and - int openHidrawSubDevice(const DeviceScan::SubDevice& sd, const DeviceId& devId) - { - constexpr int errorResult = -1; - const int devfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDWR|O_NONBLOCK , 0); - - if (devfd == errorResult) { - logWarn(device) << i18n::tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile); - return errorResult; - } - - { // Get Report Descriptor Size and Descriptor -- currently unused, but if it fails - // we don't use the device - int descriptorSize = 0; - if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0) - { - logWarn(device) << i18n::tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); - ::close(devfd); - return errorResult; - } - - struct hidraw_report_descriptor reportDescriptor {}; - reportDescriptor.size = descriptorSize; - if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0) - { - logWarn(device) << i18n::tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile); - ::close(devfd); - return errorResult; - } - } - - struct hidraw_devinfo devinfo {}; - // get the hidraw sub-device id info - if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0) - { - logWarn(device) << i18n::tr("Cannot get info from hidraw device '%1'.").arg(sd.deviceFile); - ::close(devfd); - return errorResult; - }; - - // Check against given device id - if (static_cast(devinfo.vendor) != devId.vendorId - || static_cast(devinfo.product) != devId.productId) - { - logDebug(device) << i18n::tr("Device id mismatch: %1 (%2:%3)") - .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product)); - ::close(devfd); - return errorResult; - } - - return devfd; - } + // class i18n : public QObject {}; // for i18n and logging } // ------------------------------------------------------------------------------------------------- @@ -169,15 +115,14 @@ void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) // ------------------------------------------------------------------------------------------------- SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, - const DeviceId& id, ConnectionType type, - ConnectionMode mode) - : type(type), mode(mode), parentDeviceID(id), devicePath(sd.deviceFile) + ConnectionType type, ConnectionMode mode) + : type(type), mode(mode), devicePath(sd.deviceFile) {} // ------------------------------------------------------------------------------------------------- -SubDeviceConnection::SubDeviceConnection(const DeviceScan::SubDevice& sd, const DeviceId& id, +SubDeviceConnection::SubDeviceConnection(const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode) - : m_details(sd, id, type, mode) {} + : m_details(sd, type, mode) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; @@ -200,23 +145,15 @@ DeviceFlags SubDeviceConnection::setFlags(DeviceFlags f, bool set) // ------------------------------------------------------------------------------------------------- bool SubDeviceConnection::isConnected() const { - if (type() == ConnectionType::Event) - return (m_readNotifier && m_readNotifier->isEnabled()); - if (type() == ConnectionType::Hidraw) - return (m_readNotifier && m_readNotifier->isEnabled()) && (m_writeNotifier); return false; } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::disconnect() { - m_readNotifier.reset(); - m_writeNotifier.reset(); -} - -// ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::setNotifiersEnabled(bool enabled) { - setReadNotifierEnabled(enabled); - setWriteNotifierEnabled(enabled); + if (m_readNotifier) { + m_readNotifier->setEnabled(false); + m_readNotifier.reset(); + } } // ------------------------------------------------------------------------------------------------- @@ -224,11 +161,6 @@ void SubDeviceConnection::setReadNotifierEnabled(bool enabled) { if (m_readNotifier) m_readNotifier->setEnabled(enabled); } -// ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::setWriteNotifierEnabled(bool enabled) { - if (m_writeNotifier) m_writeNotifier->setEnabled(enabled); -} - // ------------------------------------------------------------------------------------------------- const std::shared_ptr& SubDeviceConnection::inputMapper() const { return m_inputMapper; @@ -240,28 +172,19 @@ QSocketNotifier* SubDeviceConnection::socketReadNotifier() { } // ------------------------------------------------------------------------------------------------- -QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { - return m_writeNotifier.get(); -} - -// ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const QByteArray&) { - // do nothing for the base implementation - return -1; -} +void SubDeviceConnection::sendVibrateCommand(uint8_t, uint8_t) {} // ------------------------------------------------------------------------------------------------- -ssize_t SubDeviceConnection::sendData(const void*, size_t) { - // do nothing for the base implementation - return -1; -} +void SubDeviceConnection::queryBatteryStatus() {} // ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::sendVibrateCommand(uint8_t, uint8_t) {} +SubEventConnection::SubEventConnection(Token, const DeviceScan::SubDevice& sd) + : SubDeviceConnection(sd, ConnectionType::Event, ConnectionMode::ReadOnly) {} // ------------------------------------------------------------------------------------------------- -SubEventConnection::SubEventConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id) - : SubDeviceConnection(sd, id, ConnectionType::Event, ConnectionMode::ReadOnly) {} +bool SubEventConnection::isConnected() const { + return (m_readNotifier && m_readNotifier->isEnabled()); +} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd, @@ -295,7 +218,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: return std::shared_ptr(); } - auto connection = std::make_shared(Token{}, sd, dc.deviceId()); + auto connection = std::make_shared(Token{}, sd); if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; @@ -347,8 +270,25 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } // ------------------------------------------------------------------------------------------------- -SubHidrawConnection::SubHidrawConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id) - : SubDeviceConnection(sd, id, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} +SubHidrawConnection::SubHidrawConnection(Token, const DeviceScan::SubDevice& sd) + : SubDeviceConnection(sd, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} + +// ------------------------------------------------------------------------------------------------- +SubHidrawConnection::~SubHidrawConnection() = default; + +// ------------------------------------------------------------------------------------------------- +bool SubHidrawConnection::isConnected() const { + return (m_readNotifier && m_readNotifier->isEnabled()) && (m_writeNotifier); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidrawConnection::disconnect() { + SubDeviceConnection::disconnect(); + if (m_writeNotifier) { + m_writeNotifier->setEnabled(false); + m_writeNotifier.reset(); + } +} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd, @@ -357,187 +297,94 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca const int devfd = openHidrawSubDevice(sd, dc.deviceId()); if (devfd == -1) return std::shared_ptr(); - auto connection = std::make_shared(Token{}, sd, dc.deviceId()); + auto connection = std::make_shared(Token{}, sd); connection->createSocketNotifiers(devfd); - connection->m_inputMapper = dc.inputMapper(); + connect(connection->socketReadNotifier(), &QSocketNotifier::activated, + &*connection, &SubHidrawConnection::onHidrawDataAvailable); + return connection; } -// ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::queryBatteryStatus() {} - -// ------------------------------------------------------------------------------------------------- -void SubHidppConnection::pingSubDevice() +// ----------------------------------------------------------------------------------------------- +int SubHidrawConnection::openHidrawSubDevice(const DeviceScan::SubDevice& sd, const DeviceId& devId) { - constexpr uint8_t rootIndex = 0x00; // root Index is always 0x00 in any logitech device - const uint8_t pingCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rootIndex, - m_featureSet->getRandomFunctionCode(0x10), 0x00, 0x00, 0x5d}; - sendData(pingCmd, sizeof(pingCmd)); -} + constexpr int errorResult = -1; + const int devfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDWR|O_NONBLOCK , 0); -// ------------------------------------------------------------------------------------------------- -void SubHidppConnection::setHIDppProtocol(float version) { - // Inform user about the online status of device. - if (version > 0) { - if (m_details.HIDppProtocolVer < 0) + if (devfd == errorResult) { + logWarn(device) << tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile); + return errorResult; + } + + { // Get Report Descriptor Size and Descriptor -- currently unused, but if it fails + // we don't use the device + int descriptorSize = 0; + if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0) { - logDebug(hid) << "HID++ Device with path" << path() << "is now active."; - logDebug(hid) << "HID++ protocol version" << tr("%1.").arg(version); - emit activated(); + logWarn(device) << tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); + ::close(devfd); + return errorResult; } - } else { - if (m_details.HIDppProtocolVer > 0) + + struct hidraw_report_descriptor reportDescriptor {}; + reportDescriptor.size = descriptorSize; + if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0) { - logDebug(hid) << "HID++ Device with path" << path() << "got deactivated."; - emit deactivated(); + logWarn(device) << tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile); + ::close(devfd); + return errorResult; } } - m_details.HIDppProtocolVer = version; -} - -// ------------------------------------------------------------------------------------------------- -void SubHidppConnection::initialize() -{ - // Currently only HID++ devices need additional initializing - if (!hasFlags(DeviceFlag::Hidpp)) return; - constexpr int delay_ms = 20; - int msgCount = 0; - // Reset device: get rid of any device configuration by other programs ------- - if (m_details.parentDeviceID.busType == BusType::Usb) + struct hidraw_devinfo devinfo {}; + // get the hidraw sub-device id info + if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0) { - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_GET_FEATURE, 0x00, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data)); - }); - msgCount++; - - // Turn off software bit and keep the wireless notification bit on - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - constexpr uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_SET_FEATURE, 0x00, 0x00, 0x01, 0x00}; - sendData(data, sizeof(data));}); - msgCount++; - - // Initialize USB dongle - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - constexpr uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_SET_FEATURE, 0x02, 0x02, 0x00, 0x00}; - sendData(data, sizeof(data));}); - msgCount++; - - // Now enable both software and wireless notification bit - QTimer::singleShot(delay_ms*msgCount, this, [this](){ - constexpr uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_USB_RECEIVER, HIDPP::Bytes::SHORT_SET_FEATURE, 0x00, 0x00, 0x09, 0x00}; - sendData(data, sizeof(data));}); - msgCount++; - } + logWarn(device) << tr("Cannot get info from hidraw device '%1'.").arg(sd.deviceFile); + ::close(devfd); + return errorResult; + }; - DeviceFlags featureFlags = DeviceFlag::NoFlags; - // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from logitech device - setNotifiersEnabled(false); - if (m_featureSet->getFeatureCount() == 0) m_featureSet->populateFeatureTable(); - if (m_featureSet->getFeatureCount()) { - logDebug(hid) << "Loaded" << m_featureSet->getFeatureCount() << "features for" << path(); - if (m_featureSet->supportFeatureCode(FeatureCode::PresenterControl)) { - featureFlags |= DeviceFlag::Vibrate; - logDebug(hid) << "SubDevice" << path() << "reported Vibration capabilities."; - } - if (m_featureSet->supportFeatureCode(FeatureCode::BatteryStatus)) { - featureFlags |= DeviceFlag::ReportBattery; - logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; - } - if (m_featureSet->supportFeatureCode(FeatureCode::ReprogramControlsV4)) { - auto& reservedInputs = m_inputMapper->getReservedInputs(); - reservedInputs.clear(); - featureFlags |= DeviceFlags::NextHold; - featureFlags |= DeviceFlags::BackHold; - reservedInputs.emplace_back(ReservedKeyEventSequence::NextHoldInfo); - reservedInputs.emplace_back(ReservedKeyEventSequence::BackHoldInfo); - logDebug(hid) << "SubDevice" << path() << "can send next and back hold event."; - } - if (m_featureSet->supportFeatureCode(FeatureCode::PointerSpeed)) featureFlags |= DeviceFlags::PointerSpeed; - } else { - logWarn(hid) << "Loading FeatureSet for" << path() << "failed."; - logInfo(hid) << "Device might be inactive. Press any button on device to activate it."; - } - setFlags(featureFlags, true); - setReadNotifierEnabled(true); - - // Reset spotlight device - if (m_featureSet->getFeatureCount()) { - const auto resetIndex = m_featureSet->getFeatureIndex(FeatureCode::Reset); - if (resetIndex) { - QTimer::singleShot(delay_ms*msgCount, this, [this, resetIndex](){ - const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, resetIndex, m_featureSet->getRandomFunctionCode(0x10), 0x00, 0x00, 0x00}; - sendData(data, sizeof(data));}); - msgCount++; - } - } - // Device Resetting complete ------------------------------------------------- - - if (m_details.parentDeviceID.busType == BusType::Usb) { - // Ping spotlight device for checking if is online - // the response will have the version for HID++ protocol. - QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); - msgCount++; - } else if (m_details.parentDeviceID.busType == BusType::Bluetooth) { - // Bluetooth connection do not respond to ping. - // Hence, we are faking a ping response here. - // Bluetooth connection mean HID++ v2.0+. - // Setting version to 6.4: same as USB connection. - setHIDppProtocol(6.4); - } - - // Enable Next and back button on hold functionality. - const auto rcIndex = m_featureSet->getFeatureIndex(FeatureCode::ReprogramControlsV4); - if (rcIndex) { - if (hasFlags(DeviceFlags::NextHold)) { - QTimer::singleShot(delay_ms*msgCount, this, [this, rcIndex](){ - const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcIndex, m_featureSet->getRandomFunctionCode(0x30), 0x00, 0xda, 0x33, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data));}); - msgCount++; - } - - if (hasFlags(DeviceFlags::BackHold)) { - QTimer::singleShot(delay_ms*msgCount, this, [this, rcIndex](){ - const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, rcIndex, m_featureSet->getRandomFunctionCode(0x30), 0x00, 0xdc, 0x33, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - sendData(data, sizeof(data));}); - msgCount++; - } + // Check against given device id + if (static_cast(devinfo.vendor) != devId.vendorId + || static_cast(devinfo.product) != devId.productId) + { + logDebug(device) << tr("Device id mismatch: %1 (%2:%3)") + .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product)); + ::close(devfd); + return errorResult; } - // Reset pointer speed to default level of 0x04 (5th level) - if (hasFlags(DeviceFlags::PointerSpeed)) setPointerSpeed(0x04); + return devfd; } // ------------------------------------------------------------------------------------------------- ssize_t SubHidrawConnection::sendData(const QByteArray& msg) +{ + return sendData(msg.data(), msg.size()); +} + +// ------------------------------------------------------------------------------------------------- +ssize_t SubHidrawConnection::sendData(const void* msg, size_t msgLen) { constexpr ssize_t errorResult = -1; if (mode() != ConnectionMode::ReadWrite || !m_writeNotifier) { return errorResult; } + // TODO check against m_writeNotifier? + const auto res = ::write(m_writeNotifier->socket(), msg, msgLen); + logWarn(hid) << tr("Writing to '%1' len=%2 msg=%3").arg(path()).arg(msgLen).arg(qPrintable(QByteArray::fromRawData(static_cast(msg), msgLen).toHex())); - const auto notifier = socketWriteNotifier(); - const auto res = ::write(notifier->socket(), msg.data(), msg.length()); - - if (res == msg.length()) { - logDebug(hid) << res << "bytes written to" << path() << "(" << msg.toHex() << ")"; + if (static_cast(res) == msgLen) { + logDebug(hid) << res << "bytes written to" << path() << "(" + << QByteArray::fromRawData(static_cast(msg), msgLen).toHex() << ")"; } else { - logWarn(hid) << "Writing to" << path() << "failed."; + logWarn(hid) << tr("Writing to '%1' failed. (%2)").arg(path()).arg(res); } return res; } -// ------------------------------------------------------------------------------------------------- -ssize_t SubHidrawConnection::sendData(const void* msg, size_t msgLen) -{ - const QByteArray msgArr(reinterpret_cast(msg), msgLen); - return sendData(msgArr); -} - // ------------------------------------------------------------------------------------------------- void SubHidrawConnection::createSocketNotifiers(int fd) { @@ -564,119 +411,18 @@ void SubHidrawConnection::createSocketNotifiers(int fd) } // ------------------------------------------------------------------------------------------------- -SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token, - const DeviceScan::SubDevice &sd, const DeviceId &id) - : SubHidrawConnection(token, sd, id), m_featureSet(std::make_unique()) {} - -// ------------------------------------------------------------------------------------------------- -SubHidppConnection::~SubHidppConnection() = default; - -// ------------------------------------------------------------------------------------------------- - ssize_t SubHidppConnection::sendData(const QByteArray &msg) +void SubHidrawConnection::onHidrawDataAvailable(int fd) { - constexpr ssize_t errorResult = -1; - - if (!HIDPP::isValidMessage(msg)) { return errorResult; } - - // If the message have 0xff as second byte, it is meant for USB dongle hence, - // should not be send when device is connected on bluetooth. - // - // Logitech Spotlight (USB) can receive data in two different length. - // 1. Short (7 byte long starting with 0x10) - // 2. Long (20 byte long starting with 0x11) - // However, bluetooth connection only accepts data in long (20 byte) packets. - // For converting standard short length data to long length data, change the first byte to 0x11 and - // pad the end of message with 0x00 to acheive the length of 20. - - if (m_details.parentDeviceID.busType == BusType::Bluetooth) - { - if (HIDPP::isMessageForUsb(msg)) - { - logWarn(hid) << "Invalid packet" << msg.toHex() << "for spotlight connected on bluetooth."; - return errorResult; - } - - // For bluetooth always convert to a long message if we have a short message - if (HIDPP::isValidShortMessage(msg)) { - return SubHidrawConnection::sendData(HIDPP::shortToLongMsg(msg)); + QByteArray readVal(20, 0); + const auto res = ::read(fd, readVal.data(), readVal.size()); + if (res < 0) { + if (errno != EAGAIN) { + emit socketReadError(errno); } + return; } - return SubHidrawConnection::sendData(msg); -} - -// ------------------------------------------------------------------------------------------------- -std::shared_ptr SubHidppConnection::create(const DeviceScan::SubDevice &sd, - const DeviceConnection &dc) -{ - const int devfd = openHidrawSubDevice(sd, dc.deviceId()); - if (devfd == -1) return std::shared_ptr(); - - auto connection = std::make_shared(Token{}, sd, dc.deviceId()); - - if (dc.hasHidppSupport()) connection->m_details.deviceFlags |= DeviceFlag::Hidpp; - connection->m_featureSet->setHIDDeviceFileDescriptor(devfd); - - connection->createSocketNotifiers(devfd); - connection->m_inputMapper = dc.inputMapper(); - connection->postTask([c=&*connection](){ c->initialize(); }); - return connection; -} - -// ------------------------------------------------------------------------------------------------- -void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) -{ - if (!hasFlags(DeviceFlags::Vibrate)) return; - - // TODO put in HIDPP - // TODO generalize features and protocol for proprietary device features like vibration - // for not only the Spotlight device. - // - // Spotlight: - // present - // controlID len intensity - // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; - - length = length > 10? 10: length; //length should be between 0 to 10. - const uint8_t pcIndex = getFeatureSet()->getFeatureIndex(FeatureCode::PresenterControl); - using namespace HIDPP; - const uint8_t vibrateCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, pcIndex, - m_featureSet->getRandomFunctionCode(0x10), length, 0xe8, intensity}; - if (pcIndex) sendData(vibrateCmd, sizeof (vibrateCmd)); -} - -// ------------------------------------------------------------------------------------------------- -void SubHidppConnection::queryBatteryStatus() -{ - // TODO put parts in HIDPP - if (hasFlags(DeviceFlag::ReportBattery)) - { - const uint8_t batteryFeatureIndex = m_featureSet->getFeatureIndex(FeatureCode::BatteryStatus); - if (batteryFeatureIndex) - { - const uint8_t batteryCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, batteryFeatureIndex, - m_featureSet->getRandomFunctionCode(0x00), 0x00, 0x00, 0x00}; - sendData(batteryCmd, sizeof(batteryCmd)); - } - setWriteNotifierEnabled(false); - } -} - -// ------------------------------------------------------------------------------------------------- -void SubHidppConnection::setPointerSpeed(uint8_t level) -{ - const uint8_t psIndex = getFeatureSet()->getFeatureIndex(FeatureCode::PointerSpeed); - if (psIndex == 0x00) return; - - level = (level > 0x09) ? 0x09: level; // level should be in range of 0-9 - uint8_t pointerSpeed = 0x10 & level; // pointer speed sent to device are between 0x10 - 0x19 (hence ten speed levels) - const uint8_t pointerSpeedCmd[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, psIndex, - m_featureSet->getRandomFunctionCode(0x10), pointerSpeed, 0x00, 0x00}; - sendData(pointerSpeedCmd, sizeof(pointerSpeedCmd)); -} - -// ------------------------------------------------------------------------------------------------- -const HIDPP::FeatureSet* SubHidppConnection::getFeatureSet() -{ - return &*m_featureSet; + // For generic hidraw devices without known protocols, just print out the + // received data into the debug log + logDebug(hid) << "Received" << readVal.toHex() << "from" << path(); } diff --git a/src/device.h b/src/device.h index 5c567c2a..981e41e9 100644 --- a/src/device.h +++ b/src/device.h @@ -4,6 +4,8 @@ #include "asynchronous.h" #include "enum-helper.h" +#include "devicescan.h" + #include #include @@ -11,33 +13,6 @@ #include -// ------------------------------------------------------------------------------------------------- -// Bus on which device is connected -enum class BusType : uint8_t { Unknown, Usb, Bluetooth }; - -// ------------------------------------------------------------------------------------------------- -struct DeviceId -{ - uint16_t vendorId = 0; - uint16_t productId = 0; - BusType busType = BusType::Unknown; - QString phys; // should be sufficient to differentiate between two devices of the same type - // - not tested, don't have two devices of any type currently. - - inline bool operator==(const DeviceId& rhs) const { - return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); - } - - inline bool operator!=(const DeviceId& rhs) const { - return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); - } - - inline bool operator<(const DeviceId& rhs) const { - return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); - } -}; - -Q_DECLARE_METATYPE(DeviceId); // ------------------------------------------------------------------------------------------------- class InputMapper; @@ -45,13 +20,13 @@ class QSocketNotifier; class SubDeviceConnection; class VirtualDevice; -namespace HIDPP { - class FeatureSet; -} +// namespace HIDPP { +// class FeatureSet; +// } -namespace DeviceScan { - struct SubDevice; -} +// namespace DeviceScan { +// struct SubDevice; +// } // ------------------------------------------------------------------------------------------------- // Battery Status as returned on HID++ BatteryStatus feature code (0x1000) @@ -72,11 +47,9 @@ struct BatteryInfo BatteryStatus status = BatteryStatus::Discharging; }; -// ----------------------------------------------------------------------------------------------- -enum class ConnectionType : uint8_t { Event, Hidraw }; -enum class ConnectionMode : uint8_t { ReadOnly, WriteOnly, ReadWrite }; // ------------------------------------------------------------------------------------------------- +/// @brief TODO class DeviceConnection : public QObject { Q_OBJECT @@ -95,6 +68,8 @@ class DeviceConnection : public QObject void addSubDevice(std::shared_ptr); bool removeSubDevice(const QString& path); const auto& subDevices() { return m_subDeviceConnections; } + + // TODO ... battery status on device or subdevice level? void queryBatteryStatus(); auto getBatteryInfo(){ return m_batteryInfo; } @@ -114,7 +89,8 @@ public slots: QString m_deviceName; std::shared_ptr m_inputMapper; ConnectionMap m_subDeviceConnections; - BatteryInfo m_batteryInfo; + + BatteryInfo m_batteryInfo; // TODO.. }; // ------------------------------------------------------------------------------------------------- @@ -137,16 +113,14 @@ ENUM(DeviceFlag, DeviceFlags) // ------------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { - SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, const DeviceId& id, + SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode); ConnectionType type; ConnectionMode mode; - DeviceId parentDeviceID; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString devicePath; - float HIDppProtocolVer = -1; // set after ping to HID sub-device; If positive then Hidraw device is online. }; // ------------------------------------------------------------------------------------------------- @@ -172,11 +146,11 @@ class SubDeviceConnection : public QObject, public async::Async& inputMapper() const; QSocketNotifier* socketReadNotifier(); // Read notifier for Hidraw and Event connections for receiving data from device - QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device - - // Hidraw specific command - // Base implementation of generic write methods to the device does nothing. - virtual ssize_t sendData(const QByteArray& msg); - virtual ssize_t sendData(const void* msg, size_t msgLen); // HID++ specific functions: These commands write on device and expect some return message virtual bool isOnline() const { return false; }; @@ -203,16 +171,15 @@ class SubDeviceConnection : public QObject, public async::Async m_inputMapper; // shared input mapper from parent device. + std::shared_ptr m_inputMapper; ///< Shared input mapper from parent device. std::unique_ptr m_readNotifier; - std::unique_ptr m_writeNotifier; // only useful for Hidraw connections }; // ------------------------------------------------------------------------------------------------- @@ -225,7 +192,8 @@ class SubEventConnection : public SubDeviceConnection static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); - SubEventConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id); + SubEventConnection(Token, const DeviceScan::SubDevice& sd); + bool isConnected() const; auto& inputBuffer() { return m_inputEventBuffer; } protected: @@ -233,7 +201,15 @@ class SubEventConnection : public SubDeviceConnection }; // ------------------------------------------------------------------------------------------------- -class SubHidrawConnection : public SubDeviceConnection +class HidrawConnectionInterface +{ + // Generic plain, synchronous sendData interface + virtual ssize_t sendData(const QByteArray& msg) = 0; + virtual ssize_t sendData(const void* msg, size_t msgLen) = 0; +}; + +// ------------------------------------------------------------------------------------------------- +class SubHidrawConnection : public SubDeviceConnection, public HidrawConnectionInterface { Q_OBJECT @@ -244,50 +220,21 @@ class SubHidrawConnection : public SubDeviceConnection static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); - SubHidrawConnection(Token, const DeviceScan::SubDevice& sd, const DeviceId& id); + SubHidrawConnection(Token, const DeviceScan::SubDevice& sd); + virtual ~SubHidrawConnection(); + virtual bool isConnected() const override; + virtual void disconnect() override; - // Generic plain sendData implementation for hidraw devices. + // Generic plain, synchronous sendData implementation for hidraw devices. ssize_t sendData(const QByteArray& msg) override; ssize_t sendData(const void* msg, size_t msgLen) override; protected: void createSocketNotifiers(int fd); -}; - -// ------------------------------------------------------------------------------------------------- -class SubHidppConnection : public SubHidrawConnection -{ - Q_OBJECT - -public: - static std::shared_ptr create(const DeviceScan::SubDevice &sd, - const DeviceConnection &dc); - - SubHidppConnection(SubHidrawConnection::Token, const DeviceScan::SubDevice& sd, const DeviceId& id); - ~SubHidppConnection(); - - using SubHidrawConnection::sendData; - - /// sendData implementation for HIDPP devices - ssize_t sendData(const QByteArray& msg) override; - - void queryBatteryStatus() override; - void sendVibrateCommand(uint8_t intensity, uint8_t length) override; - void pingSubDevice(); - void setPointerSpeed(uint8_t level); - void setHIDppProtocol(float version); - float getHIDppProtocol() const override { return m_details.HIDppProtocolVer; }; - bool isOnline() const override { return (m_details.HIDppProtocolVer > 0); }; - - void initialize(); - - const HIDPP::FeatureSet* getFeatureSet(); - -signals: - void receivedBatteryInfo(QByteArray batteryData); - void activated(); - void deactivated(); + static int openHidrawSubDevice(const DeviceScan::SubDevice& sd, const DeviceId& devId); + std::unique_ptr m_writeNotifier; private: - std::unique_ptr m_featureSet; + void onHidrawDataAvailable(int fd); }; + diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 204d3b8b..492a93aa 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -481,6 +481,7 @@ struct InputMapper::Impl void sequenceTimeout(); void resetState(); void record(const struct input_event input_events[], size_t num); + void emitNativeKeySequence(const NativeKeySequence& ks); void execAction(const std::shared_ptr& action, DeviceKeyMap::Result r); InputMapper* m_parent = nullptr; @@ -518,7 +519,16 @@ void InputMapper::Impl::execAction(const std::shared_ptr& action, Device logDebug(input) << "Input map action, type = " << int(action->type()) << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit); - emit m_parent->actionMapped(action); + if (action->type() == Action::Type::KeySequence) + { + const auto keySequenceAction = static_cast(action.get()); + logDebug(input) << "Emitting Key Sequence:" << keySequenceAction->keySequence.toString(); + emitNativeKeySequence(keySequenceAction->keySequence); + } + else + { + emit m_parent->actionMapped(action); + } } // ------------------------------------------------------------------------------------------------- @@ -533,16 +543,23 @@ void InputMapper::Impl::sequenceTimeout() if (m_lastState.first == DeviceKeyMap::Result::Valid) { // Last input event was part of a valid key sequence, but timeout hit // So we emit our stored event so far to the virtual device - if (m_vdev) m_vdev->emitEvents(m_events); + if (m_vdev && m_events.size()) + { + m_vdev->emitEvents(m_events); + } resetState(); } else if (m_lastState.first == DeviceKeyMap::Result::PartialHit) { // Last input could have triggered an action, but we needed to wait for the timeout, since // other sequences could have been possible. - if (m_vdev) + if (m_lastState.second) + { + execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); + } + else if (m_vdev && m_events.size()) { - if (m_lastState.second) execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); - else m_vdev->emitEvents(m_events); + m_vdev->emitEvents(m_events); + m_events.resize(0); } resetState(); } @@ -555,6 +572,23 @@ void InputMapper::Impl::resetState() m_events.resize(0); } +// ------------------------------------------------------------------------------------------------- +void InputMapper::Impl::emitNativeKeySequence(const NativeKeySequence& ks) +{ + if (!m_vdev) return; + + std::vector events; + events.reserve(5); // up to 3 modifier keys + 1 key + 1 syn event + for (const auto& ke : ks.nativeSequence()) + { + for (const auto& ie : ke) + events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); + + m_vdev->emitEvents(events); + events.resize(0); + } +} + // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::record(const struct input_event input_events[], size_t num) { @@ -632,12 +666,12 @@ void InputMapper::setKeyEventInterval(int interval) // ------------------------------------------------------------------------------------------------- void InputMapper::addEvents(const input_event* input_events, size_t num) { - if (!(num && impl->m_vdev)) return; + if (num == 0 || (!impl->m_vdev)) return; // If no key mapping is configured ... if (!impl->m_recordingMode && !impl->m_keymap.hasConfig()) { // ... forward events to virtual device if it exists... - if (impl->m_vdev) impl->m_vdev->emitEvents(input_events, num); + impl->m_vdev->emitEvents(input_events, num); return; } @@ -683,8 +717,12 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) else if (res == DeviceKeyMap::Result::Hit) { // Found a valid key sequence impl->m_seqTimer->stop(); - if (const auto pos = impl->m_keymap.state()) impl->execAction(pos->action, res); - else impl->m_vdev->emitEvents(impl->m_events); + if (const auto pos = impl->m_keymap.state()) { + impl->execAction(pos->action, res); + } + else { + impl->m_vdev->emitEvents(impl->m_events); + } impl->resetState(); } @@ -701,7 +739,7 @@ void InputMapper::addEvents(KeyEvent key_event) { if (key_event.empty()) addEvents({}, 0); - auto to_input_event = [](DeviceInputEvent de){ + static const auto to_input_event = [](DeviceInputEvent de){ struct input_event ie = {{}, de.type, de.code, de.value}; return ie; }; @@ -710,8 +748,8 @@ void InputMapper::addEvents(KeyEvent key_event) if (key_event.back().type != EV_SYN) key_event.emplace_back(EV_SYN, SYN_REPORT, 0); std::vector events; - for (size_t i=0; i < key_event.size(); i++) - { + events.reserve(key_event.size()); + for (size_t i=0; i < key_event.size(); i++) { events.emplace_back(to_input_event(key_event[i])); } addEvents(events.data(), events.size()); @@ -750,24 +788,3 @@ const InputMapConfig& InputMapper::configuration() const { return impl->m_config; } - -// ------------------------------------------------------------------------------------------------- -std::shared_ptr InputMapper::getAction(KeyEventSequence kes) -{ - if (kes.empty()) return nullptr; - - KeyEventSequence kesWithoutSYNEvent; // InputMapper save KeyEventSequence without ending EV_SYN event - for (auto ke: kes) - { - if (ke.back().type == EV_SYN) ke.pop_back(); - kesWithoutSYNEvent.emplace_back(ke); - } - - auto conf = configuration(); - const auto find_it = std::find_if(conf.cbegin(), conf.cend(), [kesWithoutSYNEvent](auto c) { - return kesWithoutSYNEvent == c.first; - }); - - if (find_it != conf.cend() && find_it->second.action) return find_it->second.action; - return nullptr; -} diff --git a/src/deviceinput.h b/src/deviceinput.h index 5da33264..814aa0d6 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -324,8 +324,6 @@ class InputMapper : public QObject void setConfiguration(InputMapConfig&& config); const InputMapConfig& configuration() const; - std::shared_ptr getAction(KeyEventSequence kes); - signals: void configurationChanged(); void recordingModeChanged(bool recording); diff --git a/src/devicescan.h b/src/devicescan.h index 5e1619f8..6f81d86c 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -1,11 +1,13 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once -#include "device.h" +#include "device-defs.h" -#include +#include +#include #include +#include // ------------------------------------------------------------------------------------------------- struct SupportedDevice @@ -16,6 +18,29 @@ struct SupportedDevice QString name = {}; }; +// ------------------------------------------------------------------------------------------------- +struct DeviceId +{ + uint16_t vendorId = 0; + uint16_t productId = 0; + BusType busType = BusType::Unknown; + QString phys; // should be sufficient to differentiate between two devices of the same type + // - not tested, don't have two devices of any type currently. + + inline bool operator==(const DeviceId& rhs) const { + return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); + } + + inline bool operator!=(const DeviceId& rhs) const { + return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); + } + + inline bool operator<(const DeviceId& rhs) const { + return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); + } +}; +Q_DECLARE_METATYPE(DeviceId); + // ------------------------------------------------------------------------------------------------- namespace DeviceScan { diff --git a/src/enum-helper.h b/src/enum-helper.h index 57a7bdea..389748f4 100644 --- a/src/enum-helper.h +++ b/src/enum-helper.h @@ -1,24 +1,36 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once +#include + +/// @brief Cast enum type to underlying integral type. +template +constexpr auto to_integral(T e) { + return static_cast>(e); +} + +/// @brief Cast integral type to a given enum type. +template +constexpr auto to_enum(T v) { + return static_cast(v); +} + // ------------------------------------------------------------------------------------------------- + #define EXPAND_( x ) x // MSVC workaround #define GET_ENUM_MACRO(_1,_2,NAME,...) NAME #define ENUM(...) EXPAND_(GET_ENUM_MACRO(__VA_ARGS__, ENUM2, ENUM1)(__VA_ARGS__)) // enum flags macro (cannot be used inside class declaration) #define ENUM1(ENUMCLASS) \ inline ENUMCLASS operator|(ENUMCLASS lhs, ENUMCLASS rhs) { \ - using T = std::underlying_type_t; \ - return static_cast(static_cast(lhs) | static_cast(rhs)); } \ + return to_enum(to_integral(lhs) | to_integral(rhs)); } \ inline ENUMCLASS operator&(ENUMCLASS lhs, ENUMCLASS rhs) { \ - using T = std::underlying_type_t; \ - return static_cast(static_cast(lhs) & static_cast(rhs)); } \ + return to_enum(to_integral(lhs) & to_integral(rhs)); } \ inline ENUMCLASS operator~(ENUMCLASS lhs) { \ - using T = std::underlying_type_t; \ - return static_cast(~static_cast(lhs)); } \ + return to_enum(~to_integral(lhs)); } \ inline ENUMCLASS& operator |= (ENUMCLASS& lhs, ENUMCLASS rhs) {lhs = lhs | rhs; return lhs; } \ inline ENUMCLASS& operator &= (ENUMCLASS& lhs, ENUMCLASS rhs) {lhs = lhs & rhs; return lhs; } \ - inline bool operator!(ENUMCLASS e) { return e == static_cast(0); } + inline bool operator!(ENUMCLASS e) { return e == to_enum(0); } // enum flags macro (cannot be used inside class declaration) #define ENUM2(ENUMCLASS, PLURALNAME) \ diff --git a/src/hidpp.cc b/src/hidpp.cc index ae726afa..0e23065f 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -1,122 +1,433 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "hidpp.h" #include "logging.h" +#include "enum-helper.h" #include -#include +#include + DECLARE_LOGGING_CATEGORY(hid) +#define STRINGIFY(x) #x +#define ENUM_CASE_STRINGIFY(x) case x: return STRINGIFY(x) + // ------------------------------------------------------------------------------------------------- namespace { - using HidppMsg = uint8_t[]; - template - QByteArray make_QByteArray(const C(&a)[N]) { - return {reinterpret_cast(a),N}; + namespace Defaults { + constexpr uint8_t HidppSoftwareId = 7; + } + + // -- HID++ message offsets + namespace Offset { + constexpr uint32_t Type = 0; + constexpr uint32_t DeviceIndex = 1; + constexpr uint32_t SubId = 2; + constexpr uint32_t FeatureIndex = SubId; + constexpr uint32_t Address = 3; + + constexpr uint32_t ErrorSubId = 3; + constexpr uint32_t ErrorFeatureIndex = ErrorSubId; + constexpr uint32_t ErrorAddress = 4; + constexpr uint32_t ErrorCode = 5; + } + + namespace Defines { + constexpr uint8_t ErrorShort = 0x8f; + constexpr uint8_t ErrorLong = 0xff; + } + + uint8_t funcSwIdToByte(uint8_t function, uint8_t swId) { + return (swId & 0x0f)|((function & 0x0f) << 4); } - class Hid_ : public QObject {}; // for i18n and logging + uint8_t getRandomByte() + { + static std::mt19937 gen(std::random_device{}()); + std::uniform_int_distribution distribution; + return distribution(gen); + } + + HIDPP::Message::Data getRandomPingPayload() { + return {0, 0, getRandomByte()}; + } + +} // end anonymous namespace + +const char* HidppConnectionInterface::toString(MsgResult res) +{ + switch(res) { + ENUM_CASE_STRINGIFY(MsgResult::Ok); + ENUM_CASE_STRINGIFY(MsgResult::InvalidFormat); + ENUM_CASE_STRINGIFY(MsgResult::WriteError); + ENUM_CASE_STRINGIFY(MsgResult::Timeout); + ENUM_CASE_STRINGIFY(MsgResult::HidppError); + } + return "MsgResult::(unknown)"; } namespace HIDPP { // ------------------------------------------------------------------------------------------------- -QByteArray FeatureSet::getResponseFromDevice(const QByteArray& expectedBytes) + +const char* toString(Error e) { - if (m_fdHIDDevice == -1) return QByteArray(); - - QByteArray readVal(20, 0); - int timeOut = 4; // time out just in case device did not reply; - // 4 seconds time out is used by other programs like Solaar. - QTime timeOutTime = QTime::currentTime().addSecs(timeOut); - while(true) { - if(::read(m_fdHIDDevice, readVal.data(), readVal.length())) { - if (readVal.mid(1, 3) == expectedBytes) return readVal; - if (static_cast(readVal.at(2)) == 0x8f) return readVal; //Device not online - if (QTime::currentTime() >= timeOutTime) return QByteArray(); - } + switch(e) { + ENUM_CASE_STRINGIFY(Error::NoError); + ENUM_CASE_STRINGIFY(Error::Unknown); + ENUM_CASE_STRINGIFY(Error::InvalidArgument); + ENUM_CASE_STRINGIFY(Error::OutOfRange); + ENUM_CASE_STRINGIFY(Error::HWError); + ENUM_CASE_STRINGIFY(Error::LogitechInternal); + ENUM_CASE_STRINGIFY(Error::InvalidFeatureIndex); + ENUM_CASE_STRINGIFY(Error::InvalidFunctionId); + ENUM_CASE_STRINGIFY(Error::Busy); + ENUM_CASE_STRINGIFY(Error::Unsupported); } + return "Error::(unknown)"; } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureIndexFromDevice(FeatureCode fc) + +Message::Message() = default; + +Message::Message(Type type) + : Message(type, DeviceIndex::DefaultDevice, 0, 0, Defaults::HidppSoftwareId, {}) +{} + +Message::Message(std::vector&& data) : m_data(std::move(data)) {} + +Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, + uint8_t swId, Data payload) + : Message(Data{to_integral(type), deviceIndex, featureIndex, funcSwIdToByte(function, swId)}) { - if (m_fdHIDDevice == -1) return 0x00; + if (type == Type::Invalid) return; - const uint8_t fSetLSB = static_cast(static_cast(fc) >> 8); - const uint8_t fSetMSB = static_cast(static_cast(fc)); + m_data.reserve(m_data.size() + payload.size()); + std::move(payload.begin(), payload.end(), std::back_inserter(m_data)); - const auto featureReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, 0x00, getRandomFunctionCode(0x00), - fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }); + if (type == Type::Long) m_data.resize(20, 0x0); + else if (type == Type::Short) m_data.resize(7, 0x0); +} - const auto res = ::write(m_fdHIDDevice, featureReqMessage.data(), featureReqMessage.size()); - if (res != featureReqMessage.size()) - { - logDebug(hid) << Hid_::tr("Failed to write feature request message to device."); - return 0x00; +Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, + Data payload) + : Message(type, deviceIndex, featureIndex, function, Defaults::HidppSoftwareId, std::move(payload)) +{} + +size_t Message::size() const +{ + if (isLong()) return 20; + if (isShort()) return 7; + return 0; +} + +Message::Type Message::type() const +{ + if (isLong()) return Type::Long; + if (isShort()) return Type::Short; + return Type::Invalid; +} + +bool Message::isValid() const { return isLong() || isShort(); } + +bool Message::isShort() const { + return (m_data.size() >= 7 && m_data[Offset::Type] == to_integral(Message::Type::Short)); +} + +bool Message::isLong() const { + return (m_data.size() >= 20 && m_data[Offset::Type] == to_integral(Message::Type::Long)); +} + +bool Message::isError() const +{ + if (isShort() && m_data[Offset::SubId] == Defines::ErrorShort) { + return true; } + else if (isLong() && m_data[Offset::SubId] == Defines::ErrorLong) { + return true; + } + return false; +} + +uint8_t Message::errorSubId() const { + return m_data[Offset::ErrorSubId]; +} + +uint8_t Message::errorAddress() const { + return m_data[Offset::ErrorAddress]; +} + +uint8_t Message::errorFeatureIndex() const { + return m_data[Offset::ErrorFeatureIndex]; +} + +uint8_t Message::errorFunction() const { + return ((m_data[Offset::ErrorAddress] & 0xf0) >> 4); +} - const auto response = getResponseFromDevice(featureReqMessage.mid(1, 3)); - if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; - uint8_t featureIndex = static_cast(response.at(4)); +uint8_t Message::errorSoftwareId() const { + return (m_data[Offset::ErrorAddress] & 0x0f); +} + +HIDPP::Error Message::errorCode() const { + return to_enum(m_data[Offset::ErrorCode]); +} + +uint8_t Message::deviceIndex() const { + return m_data[Offset::DeviceIndex]; +} + +uint8_t Message::subId() const { + return m_data[Offset::SubId]; +} + +uint8_t Message::address() const { + return m_data[Offset::Address]; +} + +uint8_t Message::featureIndex() const { + return m_data[Offset::Address]; +} + +uint8_t Message::function() const { + return ((m_data[Offset::Address] & 0xf0) >> 4); +} + +uint8_t Message::softwareId() const { + return (m_data[Offset::Address] & 0x0f); +} + +void Message::setSubId(uint8_t subId) { + m_data[Offset::SubId] = subId; +} + +void Message::setAddress(uint8_t address) { + m_data[Offset::Address] = address; +} + +void Message::setFeatureIndex(uint8_t featureIndex) { + m_data[Offset::FeatureIndex] = featureIndex; +} + +void Message::setFunction(uint8_t function) { + m_data[Offset::Address] = ((function & 0x0f) << 4) | (m_data[Offset::Address] & 0x0f); +} + +void Message::setSoftwareId(uint8_t softwareId) { + m_data[Offset::Address] = (softwareId & 0x0f) | (m_data[Offset::Address] & 0xf0); +} + +bool Message::isResponseTo(const Message& other) const +{ + if (!isValid() || !other.isValid()) return false; - return featureIndex; + return deviceIndex() == other.deviceIndex() + && subId() == other.subId() + && address() == other.address(); +} + +bool Message::isErrorResponseTo(const Message& other) const +{ + if (!isValid() || !other.isValid()) return false; + + return deviceIndex() == other.deviceIndex() + && errorSubId() == other.subId() + && errorAddress() == other.address(); +} + +Message& Message::convertToLong() +{ + if (!isShort()) return *this; + + m_data.resize(20, 0); + m_data[Offset::Type] = to_integral(Type::Long); + return *this; +} + +Message Message::toLong() const { + return Message(*this).convertToLong(); +} + +QString Message::hex() const +{ + return qPrintable(QByteArray::fromRawData( + reinterpret_cast(m_data.data()), size()).toHex() + ); } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t featureSetIndex) +// ------------------------------------------------------------------------------------------------- + +FeatureSet::FeatureSet(HidppConnectionInterface* connection, QObject* parent) + : QObject(parent) + , m_connection(connection) +{} + +// ------------------------------------------------------------------------------------------------- + +void FeatureSet::getProtocolVersion(std::function cb) { - if (m_fdHIDDevice == -1) return 0x00; + if (!m_connection) { + if (cb) cb(MsgResult::WriteError, Error::Unknown, {}); + return; + } + + // Get wireless device 1 protocol version + Message reqMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, 0, 1, getRandomPingPayload()); + + m_connection->sendRequest(std::move(reqMsg), + makeSafeCallback([this, cb=std::move(cb)](MsgResult res, Message msg) { + if (cb) { + auto pv = (res == MsgResult::Ok) ? ProtocolVersion{ msg[4], msg[5] } : ProtocolVersion(); + cb(res, (res == MsgResult::HidppError) ? msg.errorCode() : Error::NoError, std::move(pv)); + } + })); +} + +void FeatureSet::getFeatureIndex(FeatureCode fc, std::function cb) +{ + postSelf([this, fc, cb=std::move(cb)]() + { - // Get Number of features (except Root Feature) supported - const auto featureCountReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x00), - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); +} - const auto res = ::write(m_fdHIDDevice, featureCountReqMessage.data(), featureCountReqMessage.size()); - if (res != featureCountReqMessage.size()) +void FeatureSet::initFromDevice() +{ + if (m_connection == nullptr) return; + + getProtocolVersion(makeSafeCallback([this](MsgResult res, Error err, ProtocolVersion pv) { - logDebug(hid) << Hid_::tr("Failed to write feature count request message to device."); - return 0x00; - } + if (err == Error::NoError) {} + logDebug(hid) << tr("ProtocolVersion = %2.%3 (%1)") + .arg(m_connection->toString(res)) + .arg(int(pv.major)) + .arg(int(pv.minor)); + m_protocolVersion = std::move(pv); + })); +} - const auto response = getResponseFromDevice(featureCountReqMessage.mid(1, 3)); - if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; - uint8_t featureCount = static_cast(response.at(4)); +// ------------------------------------------------------------------------------------------------- - return featureCount; + +FeatureSet::State FeatureSet::state() const { + return m_state; +} + +// // ------------------------------------------------------------------------------------------------- +// QByteArray FeatureSet::getResponseFromDevice(const QByteArray& expectedBytes) +// { +// if (m_connection == nullptr) return QByteArray(); + +// QByteArray readVal(20, 0); +// int timeOut = 4; // time out just in case device did not reply; +// // 4 seconds time out is used by other programs like Solaar. +// QTime timeOutTime = QTime::currentTime().addSecs(timeOut); +// while(true) { +// if(::read(m_fdHIDDevice, readVal.data(), readVal.length())) { +// if (readVal.mid(1, 3) == expectedBytes) return readVal; +// if (static_cast(readVal.at(2)) == 0x8f) return readVal; //Device not online +// if (QTime::currentTime() >= timeOutTime) return QByteArray(); +// } +// } +// } + +uint8_t FeatureSet::getFeatureIndexFromDevice(FeatureCode /* fc */) +{ + if (m_connection == nullptr) return 0x00; + // TODO implement + + // using MsgResult = HidppConnectionInterface::MsgResult; + // const uint8_t fSetLSB = static_cast(to_integral(fc) >> 8); + // const uint8_t fSetMSB = static_cast(to_integral(fc)); + + // Message::Data featureReqMessage { + // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, 0x00, getRandomFunctionCode(0x00), + // fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + // }; + + // m_connection->sendRequest(std::move(featureReqMessage), + // makeSafeCallback([this](MsgResult result, Message msg) { + // if (result != MsgResult::Ok) { + // logDebug(hid) << tr("Failed to write feature request message to device."); + // } + // Q_UNUSED(msg); + // // TODO ?? getFeatureIndex with cb?? + // })); + + // const auto res = ::write(m_fdHIDDevice, featureReqMessage.data(), featureReqMessage.size()); + // if (res != featureReqMessage.size()) + // { + // logDebug(hid) << Hid_::tr("Failed to write feature request message to device."); + // return 0x00; + // } + + // const auto response = getResponseFromDevice(featureReqMessage.mid(1, 3)); + // if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; + // uint8_t featureIndex = static_cast(response.at(4)); + // TODO + + return 0x00; //featureIndex; } // ------------------------------------------------------------------------------------------------- +uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t /* featureSetIndex */) +{ + if (m_connection == nullptr) return 0x00; + + // Get Number of features (except Root Feature) supported + // const auto featureCountReqMessage = make_QByteArray(HidppMsg{ + // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x00), + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + // }); + // TODO implement + + // const auto res = ::write(m_fdHIDDevice, featureCountReqMessage.data(), featureCountReqMessage.size()); + // if (res != featureCountReqMessage.size()) + // { + // logDebug(hid) << Hid_::tr("Failed to write feature count request message to device."); + // return 0x00; + // } + + // const auto response = getResponseFromDevice(featureCountReqMessage.mid(1, 3)); + // if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; + // uint8_t featureCount = static_cast(response.at(4)); + + // TODO return featureCount; + return 0; +} + QByteArray FeatureSet::getFirmwareVersionFromDevice() { - if (m_fdHIDDevice == -1) return 0x00; + if (m_connection == nullptr) return 0x00; // To get firmware details: first get Feature Index corresponding to Firmware feature code uint8_t fwIndex = getFeatureIndexFromDevice(FeatureCode::FirmwareVersion); if (!fwIndex) return QByteArray(); // Get the number of firmwares (Main HID++ application, BootLoader, or Hardware) now - const auto fwCountReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x00), - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }); - - const auto res = ::write(m_fdHIDDevice, fwCountReqMessage.data(), fwCountReqMessage.size()); - if (res != fwCountReqMessage.size()) - { - logDebug(hid) << Hid_::tr("Failed to write firmware count request message to device."); - return 0x00; - } - - const auto response = getResponseFromDevice(fwCountReqMessage.mid(1, 3)); - if (!response.length() || static_cast(response.at(2)) == 0x8f) return QByteArray(); - const uint8_t fwCount = static_cast(response.at(4)); - + // const auto fwCountReqMessage = make_QByteArray(HidppMsg{ + // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x00), + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + // }); + + // TODO implement + // const auto res = ::write(m_fdHIDDevice, fwCountReqMessage.data(), fwCountReqMessage.size()); + // if (res != fwCountReqMessage.size()) + // { + // logDebug(hid) << Hid_::tr("Failed to write firmware count request message to device."); + // return 0x00; + // } + + // TODO implement + // const auto response = getResponseFromDevice(fwCountReqMessage.mid(1, 3)); + // if (!response.length() || static_cast(response.at(2)) == 0x8f) return QByteArray(); + // const uint8_t fwCount = static_cast(response.at(4)); + const uint8_t fwCount = 1; + + // TODO implement // The following info is not used currently; however, these commented lines are kept for future reference. // uint8_t connectionMode = static_cast(response.at(10)); // bool supportBluetooth = (connectionMode & 0x01); @@ -131,36 +442,36 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() // if (supportUsbReceiver) { auto wpmodelID = modelIDs.mid(count, 2); count += 2;} // if (supportUsbWired) { auto usbmodelID = modelIDs.mid(count, 2); count += 2;} - + // TODO implement // Iteratively find out firmware versions for all firmwares and get the firmware for main application for (uint8_t i = 0x00; i < fwCount; i++) { - const auto fwVerReqMessage = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x10), - i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }); - - const auto res = ::write(m_fdHIDDevice, fwVerReqMessage.data(), fwVerReqMessage.length()); - if (res != fwCountReqMessage.size()) - { - logDebug(hid) << Hid_::tr("Failed to write firmware request message to device (%1).") - .arg(int(i)); - return 0x00; - } - const auto fwResponse = getResponseFromDevice(fwVerReqMessage.mid(1, 3)); - if (!fwResponse.length() || static_cast(fwResponse.at(2)) == 0x8f) return QByteArray(); - const auto fwType = (fwResponse.at(4) & 0x0f); // 0 for main HID++ application, 1 for BootLoader, 2 for Hardware, 3-15 others - const auto fwVersion = fwResponse.mid(5, 7); - // Currently we are not interested in these details; however, these commented lines are kept for future reference. - //auto firmwareName = fwVersion.mid(0, 3).data(); - //auto majorVesion = fwResponse.at(3); - //auto MinorVersion = fwResponse.at(4); - //auto build = fwResponse.mid(5); - if (fwType == 0) - { - logDebug(hid) << "Main application firmware Version:" << fwVersion.toHex(); - return fwVersion; - } + // const auto fwVerReqMessage = make_QByteArray(HidppMsg{ + // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x10), + // i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + // }); + + // const auto res = ::write(m_fdHIDDevice, fwVerReqMessage.data(), fwVerReqMessage.length()); + // if (res != fwCountReqMessage.size()) + // { + // logDebug(hid) << Hid_::tr("Failed to write firmware request message to device (%1).") + // .arg(int(i)); + // return 0x00; + // } + // const auto fwResponse = getResponseFromDevice(fwVerReqMessage.mid(1, 3)); + // if (!fwResponse.length() || static_cast(fwResponse.at(2)) == 0x8f) return QByteArray(); + // const auto fwType = (fwResponse.at(4) & 0x0f); // 0 for main HID++ application, 1 for BootLoader, 2 for Hardware, 3-15 others + // const auto fwVersion = fwResponse.mid(5, 7); + // // Currently we are not interested in these details; however, these commented lines are kept for future reference. + // //auto firmwareName = fwVersion.mid(0, 3).data(); + // //auto majorVesion = fwResponse.at(3); + // //auto MinorVersion = fwResponse.at(4); + // //auto build = fwResponse.mid(5); + // if (fwType == 0) + // { + // logDebug(hid) << "Main application firmware Version:" << fwVersion.toHex(); + // return fwVersion; + // } } return QByteArray(); } @@ -168,7 +479,7 @@ QByteArray FeatureSet::getFirmwareVersionFromDevice() // ------------------------------------------------------------------------------------------------- void FeatureSet::populateFeatureTable() { - if (m_fdHIDDevice == -1) return; + if (m_connection == nullptr) return; // Get the firmware version const auto firmwareVersion = getFirmwareVersionFromDevice(); @@ -194,31 +505,31 @@ void FeatureSet::populateFeatureTable() if (!featureCount) return; // Root feature is supported by all HID++ 2.0 device and has a featureIndex of 0 always. - m_featureTable.insert({static_cast(FeatureCode::Root), 0x00}); + m_featureTable.insert({to_integral(FeatureCode::Root), 0x00}); // Read Feature Code for other featureIndices from device. for (uint8_t featureIndex = 0x01; featureIndex <= featureCount; ++featureIndex) { - const auto featureCodeReqMsg = make_QByteArray(HidppMsg{ - HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x10), featureIndex, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }); - const auto res = ::write(m_fdHIDDevice, featureCodeReqMsg.data(), featureCodeReqMsg.size()); - if (res != featureCodeReqMsg.size()) { - logDebug(hid) << Hid_::tr("Failed to write feature code request message to device."); - return; - } - - const auto response = getResponseFromDevice(featureCodeReqMsg.mid(1, 3)); - if (!response.length() || static_cast(response.at(2)) == 0x8f) { - m_featureTable.clear(); - return; - } - const uint16_t featureCode = (static_cast(response.at(4)) << 8) | static_cast(response.at(5)); - const uint8_t featureType = static_cast(response.at(6)); - const auto softwareHidden = (featureType & (1<<6)); - const auto obsoleteFeature = (featureType & (1<<7)); - if (!(softwareHidden) && !(obsoleteFeature)) m_featureTable.insert({featureCode, featureIndex}); + // const auto featureCodeReqMsg = make_QByteArray(HidppMsg{ + // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x10), featureIndex, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + // }); + // const auto res = ::write(m_fdHIDDevice, featureCodeReqMsg.data(), featureCodeReqMsg.size()); + // if (res != featureCodeReqMsg.size()) { + // logDebug(hid) << Hid_::tr("Failed to write feature code request message to device."); + // return; + // } + + // const auto response = getResponseFromDevice(featureCodeReqMsg.mid(1, 3)); + // if (!response.length() || static_cast(response.at(2)) == 0x8f) { + // m_featureTable.clear(); + // return; + // } + // const uint16_t featureCode = (static_cast(response.at(4)) << 8) | static_cast(response.at(5)); + // const uint8_t featureType = static_cast(response.at(6)); + // const auto softwareHidden = (featureType & (1<<6)); + // const auto obsoleteFeature = (featureType & (1<<7)); + // if (!(softwareHidden) && !(obsoleteFeature)) m_featureTable.insert({featureCode, featureIndex}); } } } @@ -226,7 +537,7 @@ void FeatureSet::populateFeatureTable() // ------------------------------------------------------------------------------------------------- bool FeatureSet::supportFeatureCode(FeatureCode fc) const { - const auto featurePair = m_featureTable.find(static_cast(fc)); + const auto featurePair = m_featureTable.find(to_integral(fc)); return (featurePair != m_featureTable.end()); } @@ -235,46 +546,8 @@ uint8_t FeatureSet::getFeatureIndex(FeatureCode fc) const { if (!supportFeatureCode(fc)) return 0x00; - const auto featureInfo = m_featureTable.find(static_cast(fc)); + const auto featureInfo = m_featureTable.find(to_integral(fc)); return featureInfo->second; } -// ------------------------------------------------------------------------------------------------- -QByteArray shortToLongMsg(const QByteArray& shortMsg) -{ - const bool isValidShortMsg = (shortMsg.at(0) == Bytes::SHORT_MSG && shortMsg.length() == 7); - - if (isValidShortMsg) - { - QByteArray longMsg; - longMsg.reserve(20); - longMsg.append(Bytes::LONG_MSG); - longMsg.append(shortMsg.mid(1)); - longMsg.append(20 - longMsg.length(), 0); - return longMsg; - } - - return shortMsg; -} - -// ------------------------------------------------------------------------------------------------- -bool isValidMessage(const QByteArray& msg) { - return (isValidShortMessage(msg) || isValidLongMessage(msg)); -} - -// ------------------------------------------------------------------------------------------------- -bool isValidShortMessage(const QByteArray& msg) { - return (msg.length() == 7 && static_cast(msg.at(0)) == Bytes::SHORT_MSG); -} - -// ------------------------------------------------------------------------------------------------- -bool isValidLongMessage(const QByteArray& msg) { - return (msg.length() == 20 && static_cast(msg.at(0)) == Bytes::LONG_MSG); -} - -// ------------------------------------------------------------------------------------------------- -bool isMessageForUsb(const QByteArray& msg) { - return (static_cast(msg.at(1)) == Bytes::MSG_TO_USB_RECEIVER); -} - } // end namespace HIDPP diff --git a/src/hidpp.h b/src/hidpp.h index 838b5d76..65e3d78c 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -1,74 +1,262 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #pragma once +#include "device-defs.h" +#include "asynchronous.h" + #include +#include +#include -#include - - - -// Feature Codes important for Logitech Spotlight -enum class FeatureCode : uint16_t { - Root = 0x0000, - FeatureSet = 0x0001, - FirmwareVersion = 0x0003, - DeviceName = 0x0005, - Reset = 0x0020, - DFUControlSigned = 0x00c2, - BatteryStatus = 0x1000, - PresenterControl = 0x1a00, - Sensor3D = 0x1a01, - ReprogramControlsV4 = 0x1b04, - WirelessDeviceStatus = 0x1db4, - SwapCancelButton = 0x2005, - PointerSpeed = 0x2205, -}; +#include + +// Hidpp specific functionality +// - code is heavily inspired by this library: https://github.com/cvuchener/hidpp +// - also see https://6xq.net/git/lars/lshidpp.git +// - also see https://github.com/cvuchener/g500/blob/master/doc/hidpp10.md namespace HIDPP { - // ----------------------------------------------------------------------------------------------- - namespace Bytes { - constexpr uint8_t SHORT_MSG = 0x10; - constexpr uint8_t LONG_MSG = 0x11; + namespace DeviceIndex { + constexpr uint8_t DefaultDevice = 0xff; + constexpr uint8_t CordedDevice = 0x00; + constexpr uint8_t WirelessDevice1 = 1; + constexpr uint8_t WirelessDevice2 = 2; + constexpr uint8_t WirelessDevice3 = 3; + constexpr uint8_t WirelessDevice4 = 4; + constexpr uint8_t WirelessDevice5 = 5; + constexpr uint8_t WirelessDevice6 = 6; + } // end namespace DeviceIndex + + // see also: https://github.com/cvuchener/hidpp/blob/master/src/tools/hidpp-list-features.cpp + // Feature Codes important for Logitech Spotlight + enum class FeatureCode : uint16_t { + Root = 0x0000, + FeatureSet = 0x0001, + FirmwareVersion = 0x0003, + DeviceName = 0x0005, + Reset = 0x0020, + DFUControlSigned = 0x00c2, + BatteryStatus = 0x1000, + PresenterControl = 0x1a00, + Sensor3D = 0x1a01, + ReprogramControlsV4 = 0x1b04, + WirelessDeviceStatus = 0x1db4, + SwapCancelButton = 0x2005, + PointerSpeed = 0x2205, + }; - constexpr uint8_t MSG_TO_USB_RECEIVER = 0xff; - constexpr uint8_t MSG_TO_SPOTLIGHT = 0x01; // Spotlight is first device on the receiver (bluetooth also uses this code) + /// Hid++ 2.0 error codes + enum class Error : uint8_t { + NoError = 0, + Unknown = 1, + InvalidArgument = 2, + OutOfRange = 3, + HWError = 4, + LogitechInternal = 5, + InvalidFeatureIndex = 6, + InvalidFunctionId = 7, + Busy = 8, // Device (or receiver) busy + Unsupported = 9, + }; - constexpr uint8_t SHORT_GET_FEATURE = 0x81; - constexpr uint8_t SHORT_SET_FEATURE = 0x80; + const char* toString(Error e); - constexpr uint8_t SHORT_WIRELESS_NOTIFICATION_CODE = 0x41; + namespace Commands { + constexpr uint8_t SetRegister = 0x80; + constexpr uint8_t GetRegister = 0x81; + constexpr uint8_t SetLongRegister = 0x82; + constexpr uint8_t GetLongRegister = 0x83; } - /// Used for Bluetooth connections - QByteArray shortToLongMsg(const QByteArray& shortMsg); + /// Hidpp message class, heavily inspired by this library: https://github.com/cvuchener/hidpp + class Message final + { + public: + using Data = std::vector; + + /// HID++ message type. + enum class Type : uint8_t { + Invalid = 0x0, + Short = 0x10, + Long = 0x11, + }; + + /// Creates an invalid HID++ message object. + Message(); + /// Creates an empty default HID++ message of the given type. + /// An internal default is used as software id for the message. + Message(Type type); + /// Create a message with the given properties and payload. + Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, uint8_t swId, + Data payload); + /// Create a message with the given properties and payload. + /// An internal default is used as software id for the message. + Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, Data payload); + + /// Create a message from raw data. + /// If the data is not a valid Hidpp message, this will result in an invalid HID++ message. + Message(std::vector&& data); + + Message(Message&& msg) = default; + Message(const Message& msg) = default; + + inline bool operator==(const Message& other) const { return m_data == other.m_data; } + + bool isValid() const; + bool isLong() const; + bool isShort() const; + size_t size() const; + + bool isError() const; + // --- For short error messages (isShort() && isError()) + uint8_t errorSubId() const; + uint8_t errorAddress() const; + // -- For long error messages (isLong() && isError()) + uint8_t errorFeatureIndex() const; + uint8_t errorFunction() const; + uint8_t errorSoftwareId() const; + // --- for both long & short error messages + Error errorCode() const; + + /// Converts the message to a long message, if it is a valid short message + Message& convertToLong(); + /// Converts the message to a long message and returns it as a new object, + /// if it is a valid short message. + Message toLong() const; + + Type type() const; + uint8_t deviceIndex() const; + void setDeviceIndex(uint8_t); + + // --- HIDPP 1.0 + uint8_t subId() const; + void setSubId(uint8_t subId); + uint8_t address() const; + void setAddress(uint8_t address); + + // --- HIDPP 2.0 + uint8_t featureIndex () const; + void setFeatureIndex(uint8_t featureIndex); + uint8_t function() const; + void setFunction(uint8_t function); + uint8_t softwareId() const; + void setSoftwareId(uint8_t softwareId); + + /// Returns true if the message is a possible response to a given Hidpp message. + bool isResponseTo(const Message& other) const; + /// Returns true if the message is a possible error response to a given Hidpp message. + bool isErrorResponseTo(const Message& other) const; + + auto data() { return m_data.data(); } + auto dataSize() { return m_data.size(); } + auto operator[](size_t i) { return m_data.operator[](i); } + QString hex() const; + + private: + Data m_data; + }; +} //end of HIDPP namespace + +// ------------------------------------------------------------------------------------------------- +/// Hidpp interface to be implemented by classes that allow communicating with a HID++ device. +class HidppConnectionInterface +{ +public: + enum class MsgResult : uint8_t { + Ok = 0, + InvalidFormat, + WriteError, + Timeout, + HidppError, + }; + + static const char* toString(MsgResult); + + using SendResultCallback = std::function; + using RequestResultCallback = std::function; + + virtual BusType busType() const = 0; + + // --- synchronous versions + virtual ssize_t sendData(std::vector msg) = 0; + virtual ssize_t sendData(HIDPP::Message msg) = 0; + + // --- asynchronous versions, implementations must return immediately + virtual void sendData(std::vector msg, SendResultCallback resultCb) = 0; + virtual void sendData(HIDPP::Message msg, SendResultCallback resultCb) = 0; + virtual void sendRequest(std::vector msg, RequestResultCallback responseCb) = 0; + virtual void sendRequest(HIDPP::Message msg, RequestResultCallback responseCb) = 0; - /// Returns if msg is a valid hidpp message - bool isValidMessage(const QByteArray& msg); - bool isValidShortMessage(const QByteArray& msg); - bool isValidLongMessage(const QByteArray& msg); + struct RequestBatchItem { + HIDPP::Message message; + RequestResultCallback callback; + }; + + using RequestBatch = std::queue; + using RequestBatchResultCallback = std::function)>; + virtual void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, + bool continueOnError = false) = 0; + + struct DataBatchItem { + HIDPP::Message message; + SendResultCallback callback; + }; - bool isMessageForUsb(const QByteArray& msg); + using DataBatch = std::queue; + using DataBatchResultCallback = std::function)>; + virtual void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, + bool continueOnError = false) = 0; +}; + +namespace HIDPP { + // ----------------------------------------------------------------------------------------------- + namespace Bytes { + constexpr uint8_t SHORT_WIRELESS_NOTIFICATION_CODE = 0x41; // TODO MOVE RENAME /// + } - // Class to get and store Set of supported features for a HID++ 2.0 device - class FeatureSet + /// Class to get and store set of supported features and additional information + /// for a HID++ 2.0 device (although very much specialized for the Logitech Spotlight). + class FeatureSet : public QObject, public async::Async { + Q_OBJECT + public: - void setHIDDeviceFileDescriptor(int fd) { m_fdHIDDevice = fd; } + enum class State : uint8_t { Uninitialized, Initializing, Initialized, Error }; + + FeatureSet(HidppConnectionInterface* connection, QObject* parent = nullptr); + + void initFromDevice(); + State state() const; + uint8_t getFeatureIndex(FeatureCode fc) const; bool supportFeatureCode(FeatureCode fc) const; auto getFeatureCount() const { return m_featureTable.size(); } - uint8_t getRandomFunctionCode(uint8_t functionCode) const { return (functionCode | m_softwareIDBits); } void populateFeatureTable(); + signals: + void stateChanged(State s); + private: + struct ProtocolVersion { + uint8_t major = 0; + uint8_t minor = 0; + }; + using MsgResult = HidppConnectionInterface::MsgResult; + void getFeatureIndex(FeatureCode fc, std::function cb); + + void getProtocolVersion(std::function cb); + uint8_t getFeatureIndexFromDevice(FeatureCode fc); uint8_t getFeatureCountFromDevice(uint8_t featureSetID); QByteArray getFirmwareVersionFromDevice(); - QByteArray getResponseFromDevice(const QByteArray &expectedBytes); + HidppConnectionInterface* m_connection = nullptr; std::map m_featureTable; - int m_fdHIDDevice = -1; - uint8_t m_softwareIDBits = (rand() & 0x0f); - }; -} //end of HIDPP namespace + ProtocolVersion m_protocolVersion; + + State m_state = State::Uninitialized; + }; +} //end namespace HIDPP diff --git a/src/iconwidgets.h b/src/iconwidgets.h index e179627a..133ee6d5 100644 --- a/src/iconwidgets.h +++ b/src/iconwidgets.h @@ -7,6 +7,7 @@ #include // ------------------------------------------------------------------------------------------------- +/// @brief Icon button class used throughout the application's widget based dialogs. class IconButton : public QToolButton { Q_OBJECT @@ -16,6 +17,7 @@ class IconButton : public QToolButton }; // ------------------------------------------------------------------------------------------------- +/// @brief Icon label class used throughout the application's widget based dialogs. class IconLabel : public QLabel { Q_OBJECT diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index 500ea0a2..f092dfbe 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -280,7 +280,6 @@ void InputMapConfigModel::updateDuplicates() } } -#include // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- InputMapConfigView::InputMapConfigView(QWidget* parent) diff --git a/src/inputmapconfig.h b/src/inputmapconfig.h index 17e25985..f138f0d6 100644 --- a/src/inputmapconfig.h +++ b/src/inputmapconfig.h @@ -8,10 +8,12 @@ #include // ------------------------------------------------------------------------------------------------- + class ActionTypeDelegate; class InputSeqDelegate; // ------------------------------------------------------------------------------------------------- +/// @brief TODO struct InputMapModelItem { KeyEventSequence deviceSequence; std::shared_ptr action; @@ -19,6 +21,7 @@ struct InputMapModelItem { }; // ------------------------------------------------------------------------------------------------- +/// @brief TODO class InputMapConfigModel : public QAbstractTableModel { Q_OBJECT @@ -60,6 +63,7 @@ class InputMapConfigModel : public QAbstractTableModel }; // ------------------------------------------------------------------------------------------------- +/// @brief TODO struct InputMapConfigView : public QTableView { Q_OBJECT diff --git a/src/main.cc b/src/main.cc index 51da7831..36cf596b 100644 --- a/src/main.cc +++ b/src/main.cc @@ -12,6 +12,7 @@ #include #endif +#include #include #include @@ -39,6 +40,14 @@ namespace { auto& operator<<(const T& a) const { return std::cerr << a; } ~error() { std::cerr << std::endl; } }; + + void ctrl_c_signal_handler(int sig) + { + if (sig == SIGINT) { + print() << "..."; + if (qApp) QCoreApplication::quit(); + } + } } int main(int argc, char *argv[]) @@ -326,5 +335,6 @@ int main(int argc, char *argv[]) } ProjecteurApplication app(argc, argv, options); + signal(SIGINT, ctrl_c_signal_handler); return app.exec(); } diff --git a/src/projecteurapp.h b/src/projecteurapp.h index f1a6acdf..487a8b70 100644 --- a/src/projecteurapp.h +++ b/src/projecteurapp.h @@ -3,6 +3,7 @@ #include "spotlight.h" #include +#include #include #include diff --git a/src/spotlight.cc b/src/spotlight.cc index 97825535..1f12bee8 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -1,7 +1,8 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "spotlight.h" -#include "hidpp.h" +#include "device.h" +#include "device-hidpp.h" #include "logging.h" #include "settings.h" #include "virtualdevice.h" @@ -9,7 +10,6 @@ #include #include #include -#include #include #include @@ -145,7 +145,9 @@ int Spotlight::connectDevices() if (dc->hasHidppSupport()) { auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc); - if (addHidppInputHandler(hidppCon)) + if (true + //addHidppInputHandler(hidppCon) + ) { // connect to hidpp sub connection signals connect(&*hidppCon, &SubHidppConnection::receivedBatteryInfo, @@ -199,34 +201,6 @@ int Spotlight::connectDevices() { if (!(action->isRepeated()) && m_holdButtonStatus.numEvents() > 0) return; - auto emitNativeKeySequence = [this](const NativeKeySequence& ks) - { - if (!m_virtualDevice) return; - - std::vector events; - events.reserve(5); // up to 3 modifier keys + 1 key + 1 syn event - for (const auto& ke : ks.nativeSequence()) - { - for (const auto& ie : ke) - events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); - - m_virtualDevice->emitEvents(events); - events.resize(0); - }; - }; - - if (action->type() == Action::Type::KeySequence) - { - if (!m_virtualDevice) return; - - const auto keySequenceAction = static_cast(action.get()); - if (!keySequenceAction->keySequence.empty()) - { - logDebug(input) << "Emitting Key Sequence:" << keySequenceAction->keySequence.toString(); - emitNativeKeySequence(keySequenceAction->keySequence); - } - } - if (action->type() == Action::Type::CyclePresets) { auto it = std::find(m_settings->presets().cbegin(), m_settings->presets().cend(), lastPreset); @@ -240,13 +214,11 @@ int Spotlight::connectDevices() m_settings->loadPreset(lastPreset); } } - - if (action->type() == Action::Type::ToggleSpotlight) + else if (action->type() == Action::Type::ToggleSpotlight) { m_settings->setOverlayDisabled(!m_settings->overlayDisabled()); } - - if (action->type() == Action::Type::ScrollHorizontal || action->type() == Action::Type::ScrollVertical) + else if (action->type() == Action::Type::ScrollHorizontal || action->type() == Action::Type::ScrollVertical) { if (!m_virtualDevice) return; @@ -259,8 +231,7 @@ int Spotlight::connectDevices() if (param) m_virtualDevice->emitEvents(scrollInputEvents); } - - if (action->type() == Action::Type::VolumeControl) + else if (action->type() == Action::Type::VolumeControl) { if (!m_virtualDevice) return; @@ -344,7 +315,7 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) if (errno != EAGAIN) { const bool anyConnectedBefore = anySpotlightDeviceConnected(); - connection.setNotifiersEnabled(false); + connection.disconnect(); QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ removeDeviceConnection(devicePath); if (!anySpotlightDeviceConnected() && anyConnectedBefore) { @@ -387,147 +358,149 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) } // end while loop } -// ------------------------------------------------------------------------------------------------- -void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) -{ - Q_UNUSED(fd); - Q_UNUSED(connection); - QByteArray readVal(20, 0); - if (::read(fd, static_cast(readVal.data()), readVal.length()) < 0) - { - if (errno != EAGAIN) - { - const bool anyConnectedBefore = anySpotlightDeviceConnected(); - connection.setNotifiersEnabled(false); - QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ - removeDeviceConnection(devicePath); - if (!anySpotlightDeviceConnected() && anyConnectedBefore) { - emit anySpotlightDeviceConnectedChanged(false); - } - }); - } - return; - } - - // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) - if (!(readVal.at(0) == HIDPP::Bytes::SHORT_MSG || readVal.at(0) == HIDPP::Bytes::LONG_MSG)) { - return; - } - - logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - - if (readVal.at(0) == HIDPP::Bytes::SHORT_MSG) // Logitech HIDPP SHORT message: 7 byte long - { - // wireless notification from USB dongle - if (readVal.at(2) == HIDPP::Bytes::SHORT_WIRELESS_NOTIFICATION_CODE) { - auto connection_status = readVal.at(4) & (1<<6); // should be zero for working connection between - // USB dongle and Spotlight device. - if (connection_status) { // connection between USB dongle and spotlight device broke - connection.setHIDppProtocol(-1); - } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. - if (!connection.isOnline()) connection.initialize(); - } - } - } - - if (readVal.at(0) == HIDPP::Bytes::LONG_MSG) // Logitech HIDPP LONG message: 20 byte long - { - // response to ping - auto rootIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::Root); - if (readVal.at(2) == rootIndex) { - if (readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10) && readVal.at(6) == 0x5d) { - auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; - connection.setHIDppProtocol(protocolVer); - } - } - - // Wireless Notification from the Spotlight device - auto wnIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::WirelessDeviceStatus); - if (wnIndex && readVal.at(2) == wnIndex) { // Logitech spotlight presenter unit got online. - if (!connection.isOnline()) connection.initialize(); - } - - // Battery packet processing: Device responded to BatteryStatus (0x1000) packet - auto batteryIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::BatteryStatus); - if (batteryIndex && readVal.at(2) == batteryIndex && - readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x00)) { // Battery information packet - QByteArray batteryData(readVal.mid(4, 3)); - emit connection.receivedBatteryInfo(batteryData); - } - - // Process reprogrammed keys : Next Hold and Back Hold - auto rcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::ReprogramControlsV4); - if (rcIndex && readVal.at(2) == rcIndex) // Button (for which hold events are on) related message. - { - auto eventCode = static_cast(readVal.at(3)); - auto buttonCode = static_cast(readVal.at(5)); - if (eventCode == 0x00) { // hold start/stop events - switch (buttonCode) { - case 0xda: - logDebug(hid) << "Next Hold Event "; - m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Next); - break; - case 0xdc: - logDebug(hid) << "Back Hold Event "; - m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Back); - break; - case 0x00: - // hold event over. - logDebug(hid) << "Hold Event over."; - m_holdButtonStatus.reset(); - } - } - else if (eventCode == 0x10) { // mouse move event - // Mouse data is sent as 4 byte information starting at 5th byte and ending at 8th. - // out of these 6th byte and 8th bytes are x and y relative change, respectively. - // Not sure about meaning of 5th and 7th bytes. However during testing - // the 5th byte shows horizonal scroll towards right if rel value is -1 otherwise left scroll (0) - // the 7th byte shows vertical scroll towards up if rel value is -1 otherwise down scroll (0) - auto byteToRel = [](int i){return ( (i<128) ? i : 256-i);}; // convert the byte to relative motion in x or y - int x = byteToRel(readVal.at(5)); - int y = byteToRel(readVal.at(7)); - auto action = connection.inputMapper()->getAction(m_holdButtonStatus.keyEventSeq()); - - if (action && !action->empty()) - { - auto getReducedParam = [](int param, int limit=2){ // reduce the values from Spotlight device for better scroll behavior - int minVal=5; - if (abs(param) < minVal) return 0; // ignore small device movement - - auto sign = (param == 0)? 0: ((param > 0)? 1:-1); - return ((abs(param) > minVal*limit)? sign*minVal*limit : param)/minVal; // limit return value between -limit to limit - }; - - if (action->type() == Action::Type::ScrollHorizontal) - { - const auto scrollHAction = static_cast(action.get()); - scrollHAction->param = -(getReducedParam(x)); - } - if (action->type() == Action::Type::ScrollVertical) - { - const auto scrollVAction = static_cast(action.get()); - scrollVAction->param = getReducedParam(y); - } - if(action->type() == Action::Type::VolumeControl) - { - const auto volumeControlAction = static_cast(action.get()); - volumeControlAction->param = -getReducedParam(y, 3); - } - - // feed the keystroke to InputMapper and let it trigger the associated action - for (auto key_event: m_holdButtonStatus.keyEventSeq()) connection.inputMapper()->addEvents(key_event); - } - m_holdButtonStatus.addEvent(); - } - } - - // Vibration response check - const uint8_t pcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::PresenterControl); - if (pcIndex && readVal.at(2) == pcIndex && readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10)) { - logDebug(hid) << "Device acknowledged a vibration event."; - } - } -} +// // ------------------------------------------------------------------------------------------------- +// void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) +// { +// Q_UNUSED(fd); +// Q_UNUSED(connection); +// QByteArray readVal(20, 0); +// if (::read(fd, static_cast(readVal.data()), readVal.length()) < 0) +// { +// if (errno != EAGAIN) +// { +// const bool anyConnectedBefore = anySpotlightDeviceConnected(); +// connection.disconnect(); +// QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ +// removeDeviceConnection(devicePath); +// if (!anySpotlightDeviceConnected() && anyConnectedBefore) { +// emit anySpotlightDeviceConnectedChanged(false); +// } +// }); +// } +// return; +// } + +// // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) +// if (!(readVal.at(0) == HIDPP::Bytes::SHORT_MSG || readVal.at(0) == HIDPP::Bytes::LONG_MSG)) { +// return; +// } + +// logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); + +// if (readVal.at(0) == HIDPP::Bytes::SHORT_MSG) // Logitech HIDPP SHORT message: 7 byte long +// { +// // wireless notification from USB dongle +// if (readVal.at(2) == HIDPP::Bytes::SHORT_WIRELESS_NOTIFICATION_CODE) { +// auto connection_status = readVal.at(4) & (1<<6); // should be zero for working connection between +// // USB dongle and Spotlight device. +// if (connection_status) { // connection between USB dongle and spotlight device broke +// connection.setHIDppProtocol(-1); +// } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. +// if (!connection.isOnline()) connection.initialize(); +// } +// } +// } + +// if (readVal.at(0) == HIDPP::Bytes::LONG_MSG) // Logitech HIDPP LONG message: 20 byte long +// { +// // response to ping +// auto rootIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::Root); +// if (readVal.at(2) == rootIndex) { +// if (readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10) && readVal.at(6) == 0x5d) { +// auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; +// connection.setHIDppProtocol(protocolVer); +// } +// } + +// // Wireless Notification from the Spotlight device +// auto wnIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::WirelessDeviceStatus); +// if (wnIndex && readVal.at(2) == wnIndex) { // Logitech spotlight presenter unit got online. +// if (!connection.isOnline()) connection.initialize(); +// } + +// // Battery packet processing: Device responded to BatteryStatus (0x1000) packet +// auto batteryIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::BatteryStatus); +// if (batteryIndex && readVal.at(2) == batteryIndex && +// readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x00)) { // Battery information packet +// QByteArray batteryData(readVal.mid(4, 3)); +// emit connection.receivedBatteryInfo(batteryData); +// } + +// // Process reprogrammed keys : Next Hold and Back Hold +// auto rcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::ReprogramControlsV4); +// if (rcIndex && readVal.at(2) == rcIndex) // Button (for which hold events are on) related message. +// { +// auto eventCode = static_cast(readVal.at(3)); +// auto buttonCode = static_cast(readVal.at(5)); +// if (eventCode == 0x00) { // hold start/stop events +// switch (buttonCode) { +// case 0xda: +// logDebug(hid) << "Next Hold Event "; +// m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Next); +// break; +// case 0xdc: +// logDebug(hid) << "Back Hold Event "; +// m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Back); +// break; +// case 0x00: +// // hold event over. +// logDebug(hid) << "Hold Event over."; +// m_holdButtonStatus.reset(); +// } +// } +// else if (eventCode == 0x10) { // mouse move event +// // Mouse data is sent as 4 byte information starting at 5th byte and ending at 8th. +// // out of these 6th byte and 8th bytes are x and y relative change, respectively. +// // Not sure about meaning of 5th and 7th bytes. However during testing +// // the 5th byte shows horizonal scroll towards right if rel value is -1 otherwise left scroll (0) +// // the 7th byte shows vertical scroll towards up if rel value is -1 otherwise down scroll (0) +// auto byteToRel = [](int i){return ( (i<128) ? i : 256-i);}; // convert the byte to relative motion in x or y +// int x = byteToRel(readVal.at(5)); +// int y = byteToRel(readVal.at(7)); + +// //auto action = connection.inputMapper()->getAction(m_holdButtonStatus.keyEventSeq()); +// auto action = std::shared_ptr{}; + +// if (action && !action->empty()) +// { +// auto getReducedParam = [](int param, int limit=2){ // reduce the values from Spotlight device for better scroll behavior +// int minVal=5; +// if (abs(param) < minVal) return 0; // ignore small device movement + +// auto sign = (param == 0)? 0: ((param > 0)? 1:-1); +// return ((abs(param) > minVal*limit)? sign*minVal*limit : param)/minVal; // limit return value between -limit to limit +// }; + +// if (action->type() == Action::Type::ScrollHorizontal) +// { +// const auto scrollHAction = static_cast(action.get()); +// scrollHAction->param = -(getReducedParam(x)); +// } +// if (action->type() == Action::Type::ScrollVertical) +// { +// const auto scrollVAction = static_cast(action.get()); +// scrollVAction->param = getReducedParam(y); +// } +// if(action->type() == Action::Type::VolumeControl) +// { +// const auto volumeControlAction = static_cast(action.get()); +// volumeControlAction->param = -getReducedParam(y, 3); +// } + +// // feed the keystroke to InputMapper and let it trigger the associated action +// for (auto key_event: m_holdButtonStatus.keyEventSeq()) connection.inputMapper()->addEvents(key_event); +// } +// m_holdButtonStatus.addEvent(); +// } +// } + +// // Vibration response check +// const uint8_t pcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::PresenterControl); +// if (pcIndex && readVal.at(2) == pcIndex && readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10)) { +// logDebug(hid) << "Device acknowledged a vibration event."; +// } +// } +// } // ------------------------------------------------------------------------------------------------- bool Spotlight::addInputEventHandler(std::shared_ptr connection) @@ -545,23 +518,23 @@ bool Spotlight::addInputEventHandler(std::shared_ptr connect return true; } -// ------------------------------------------------------------------------------------------------- -bool Spotlight::addHidppInputHandler(std::shared_ptr connection) -{ - if (!connection || connection->type() != ConnectionType::Hidraw - || !connection->isConnected() || !connection->hasFlags(DeviceFlag::Hidpp)) - { - return false; - } - - QSocketNotifier* const readNotifier = connection->socketReadNotifier(); - connect(readNotifier, &QSocketNotifier::activated, this, - [this, connection=std::move(connection)](int fd) { - onHidppDataAvailable(fd, *connection.get()); - }); - - return true; -} +// // ------------------------------------------------------------------------------------------------- +// bool Spotlight::addHidppInputHandler(std::shared_ptr connection) +// { +// if (!connection || connection->type() != ConnectionType::Hidraw +// || !connection->isConnected() || !connection->hasFlags(DeviceFlag::Hidpp)) +// { +// return false; +// } + +// QSocketNotifier* const readNotifier = connection->socketReadNotifier(); +// connect(readNotifier, &QSocketNotifier::activated, this, +// [this, connection=std::move(connection)](int fd) { +// onHidppDataAvailable(fd, *connection.get()); +// }); + +// return true; +// } // ------------------------------------------------------------------------------------------------- bool Spotlight::setupDevEventInotify() diff --git a/src/spotlight.h b/src/spotlight.h index 923f8a4f..c5a01c89 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -13,6 +13,8 @@ class QTimer; class Settings; class VirtualDevice; +class DeviceConnection; +class SubEventConnection; // ----------------------------------------------------------------------------------------------- struct HoldButtonStatus { @@ -84,13 +86,13 @@ class Spotlight : public QObject ConnectionResult connectSpotlightDevice(const QString& devicePath, bool verbose = false); bool addInputEventHandler(std::shared_ptr connection); - bool addHidppInputHandler(std::shared_ptr connection); + // bool addHidppInputHandler(std::shared_ptr connection); bool setupDevEventInotify(); int connectDevices(); void removeDeviceConnection(const QString& devicePath); void onEventDataAvailable(int fd, SubEventConnection& connection); - void onHidppDataAvailable(int fd, SubHidppConnection& connection); + // void onHidppDataAvailable(int fd, SubHidppConnection& connection); const Options m_options; std::map> m_deviceConnections; From 47a4075570fd837f19ea8addbded602fc74677c2 Mon Sep 17 00:00:00 2001 From: Jahn Date: Mon, 6 Sep 2021 21:57:19 +0200 Subject: [PATCH 063/110] Refactoring of hid++ functionality part 2. --- src/device-hidpp.cc | 450 ++++++++++++++++++++++-------- src/device-hidpp.h | 54 +++- src/device.cc | 25 +- src/device.h | 5 +- src/deviceinput.cc | 3 +- src/deviceinput.h | 3 +- src/devicescan.cc | 4 +- src/devicescan.h | 3 +- src/deviceswidget.cc | 4 +- src/deviceswidget.h | 3 +- src/enum-helper.h | 9 +- src/extra-devices.cc.in | 3 +- src/hidpp.cc | 596 ++++++++++++++++++++++++---------------- src/hidpp.h | 110 ++++++-- src/iconwidgets.h | 4 +- src/inputmapconfig.h | 6 +- src/spotlight.cc | 42 ++- 17 files changed, 879 insertions(+), 445 deletions(-) diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index d823e072..e593cd5f 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -4,10 +4,11 @@ #include "device-hidpp.h" #include "logging.h" -#include - +#include "enum-helper.h" #include "deviceinput.h" +#include + #include #include @@ -29,6 +30,33 @@ SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token, // ------------------------------------------------------------------------------------------------- SubHidppConnection::~SubHidppConnection() = default; +// ------------------------------------------------------------------------------------------------- +const char* toString(SubHidppConnection::ReceiverState s) +{ + using ReceiverState = SubHidppConnection::ReceiverState; + switch (s) { + ENUM_CASE_STRINGIFY(ReceiverState::Uninitialized); + ENUM_CASE_STRINGIFY(ReceiverState::Initializing); + ENUM_CASE_STRINGIFY(ReceiverState::Initialized); + ENUM_CASE_STRINGIFY(ReceiverState::Error); + } + return "ReceiverState::(unknown)"; +} + +const char* toString(SubHidppConnection::PresenterState s) +{ + using PresenterState = SubHidppConnection::PresenterState; + switch (s) { + ENUM_CASE_STRINGIFY(PresenterState::Uninitialized); + ENUM_CASE_STRINGIFY(PresenterState::Uninitialized_Offline); + ENUM_CASE_STRINGIFY(PresenterState::Initializing); + ENUM_CASE_STRINGIFY(PresenterState::Initialized_Online); + ENUM_CASE_STRINGIFY(PresenterState::Initialized_Offline); + ENUM_CASE_STRINGIFY(PresenterState::Error); + } + return "PresenterState::(unknown)"; +} + // ------------------------------------------------------------------------------------------------- ssize_t SubHidppConnection::sendData(std::vector data) { return sendData(HIDPP::Message(std::move(data))); @@ -42,20 +70,19 @@ ssize_t SubHidppConnection::sendData(HIDPP::Message msg) // return errorResult; } - // If the message have 0xff as second byte, it is meant for USB dongle hence, - // should not be send when device is connected on bluetooth. + // If the message has the device index 0xff it is meant for USB dongle. + // We should not be send it, when the device is connected via bluetooth. // - // Logitech Spotlight (USB) can receive data in two different length. + // The Logitech Spotlight (USB) can receive data in two different lengths: // 1. Short (7 byte long starting with 0x10) // 2. Long (20 byte long starting with 0x11) - // However, bluetooth connection only accepts data in long (20 byte) packets. - // For converting standard short length data to long length data, change the first byte to 0x11 - // and pad the end of message with 0x00 to acheive the length of 20. + // However, the bluetooth connection only accepts data in long (20 byte) messages. if (m_busType == BusType::Bluetooth) { if (msg.deviceIndex() == HIDPP::DeviceIndex::DefaultDevice) { - logWarn(hid) << "Invalid packet" << msg.hex() << "for spotlight connected on bluetooth."; + logWarn(hid) << tr("Invalid message device index in data '%1' for device connected " + "via bluetooth.").arg(msg.hex()); return errorResult; } @@ -66,7 +93,6 @@ ssize_t SubHidppConnection::sendData(HIDPP::Message msg) // return SubHidrawConnection::sendData(msg.data(), msg.size()); } - // ------------------------------------------------------------------------------------------------- void SubHidppConnection::sendData(std::vector data, SendResultCallback resultCb) { sendData(HIDPP::Message(std::move(data)), std::move(resultCb)); @@ -110,30 +136,47 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r return; } + // Device index sanity check + static const std::array validDeviceIndexes { + HIDPP::DeviceIndex::CordedDevice, + HIDPP::DeviceIndex::DefaultDevice, + HIDPP::DeviceIndex::WirelessDevice1, + }; + + const auto deviceIndexIt + = std::find(validDeviceIndexes.cbegin(), validDeviceIndexes.cend(), msg.deviceIndex()); + + if (deviceIndexIt == validDeviceIndexes.cend()) + { + logWarn(hid) << tr("Invalid device index (%1) in message for '%2'") + .arg(msg.deviceIndex()).arg(path()); + if (cb) cb(MsgResult::InvalidFormat, HIDPP::Message()); + return; + } + if (m_busType == BusType::Bluetooth) { // For bluetooth always convert to a long message if we have a short message msg.convertToLong(); } - // TODO: more early sanity checks?? device index in a valid range??? - - sendData(msg, [this, msg](MsgResult result) { - if (result == MsgResult::Ok) return; + sendData(msg, makeSafeCallback([this, msg](MsgResult result) + { + // If data was sent successfully the request will be handled when the reply arrives or + // the request times out -> return + if (result == MsgResult::Ok) { return; } - // error result, find msg in request list + // error result, find our message in the request list auto it = std::find_if(m_requests.begin(), m_requests.end(), [&msg](const RequestEntry& entry) { return entry.request == msg; }); if (it == m_requests.end()) { - // TODO log warning send error for message without matching request + logDebug(hid) << "Send request write error without matching request queue entry."; return; } - if (it->callBack) { - it->callBack(result, HIDPP::Message()); - } + if (it->callBack) { it->callBack(result, HIDPP::Message()); } m_requests.erase(it); - }); + })); // Place request in request list with a timeout m_requests.emplace_back(RequestEntry{ @@ -256,7 +299,7 @@ std::shared_ptr SubHidppConnection::create(const DeviceScan: connect(connection->socketReadNotifier(), &QSocketNotifier::activated, &*connection, &SubHidppConnection::onHidppDataAvailable); - connection->postTask([c = &*connection]() { c->initialize(); }); + connection->postTask([c = &*connection]() { c->subDeviceInit(); }); return connection; } @@ -274,7 +317,7 @@ void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) { // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; length = length > 10 ? 10 : length; // length should be between 0 to 10. - const uint8_t pcIndex = m_featureSet.getFeatureIndex(HIDPP::FeatureCode::PresenterControl); + const uint8_t pcIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PresenterControl); using namespace HIDPP; // const uint8_t vibrateCmd[] = {HIDPP::Bytes::SHORT_MSG, // HIDPP::Bytes::MSG_TO_SPOTLIGHT, @@ -314,7 +357,7 @@ void SubHidppConnection::setPointerSpeed(uint8_t //level ) { - const uint8_t psIndex = m_featureSet.getFeatureIndex(HIDPP::FeatureCode::PointerSpeed); + const uint8_t psIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PointerSpeed); if (psIndex == 0x00) return; // level = (level > 0x09) ? 0x09 : level; // level should be in range of 0-9 @@ -330,78 +373,171 @@ void SubHidppConnection::setPointerSpeed(uint8_t } // ------------------------------------------------------------------------------------------------- +void SubHidppConnection::setReceiverState(ReceiverState rs) +{ + if (rs == m_receiverState) return; + + logDebug(hid) << tr("Receiver state (%1) changes from %3 to %4") + .arg(path()).arg(toString(m_receiverState), toString(rs)); + m_receiverState = rs; + emit receiverStateChanged(m_receiverState); +} -void SubHidppConnection::initUsbReceiver(std::function cb) +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::setPresenterState(PresenterState ps) { - if (m_busType != BusType::Usb) - { - if (cb) cb(MsgResult::Ok); - return; - } + if (ps == m_presenterState) return; - using namespace HIDPP; - using Type = HIDPP::Message::Type; - RequestBatch batch{{ - RequestBatchItem{ - // Reset device: get rid of any device configuration by other programs - Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 0, {}), - makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; - logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); - }) - }, - RequestBatchItem{ - // Turn off software bit and keep the wireless notification bit on - Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x01, 0x00}), - makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; - logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); - }) - }, - RequestBatchItem{ - // Initialize USB dongle - Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 2, {}), - makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; - logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); - }) - }, - RequestBatchItem{ - // --- - Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 2, {0x02, 0x00, 0x00}), - makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; - logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); - }) - }, - RequestBatchItem{ - // Now enable both software and wireless notification bit - Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x09, 0x00}), - makeSafeCallback([](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; - logWarn(hid) << tr("Usb Receiver init failure - %1").arg(toString(result)); - }) - }, - }}; - - sendRequestBatch(std::move(batch), [cb=std::move(cb)](std::vector results) { - if (cb) cb(results.back()); + logDebug(hid) << tr("Presenter state (%1) changes from %2 to %3") + .arg(path()).arg(toString(m_presenterState), toString(ps)); + m_presenterState = ps; + emit presenterStateChanged(m_presenterState); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::initReceiver(std::function cb) +{ + postSelf([this, cb=std::move(cb)](){ + if (m_receiverState == ReceiverState::Initializing + || m_receiverState == ReceiverState::Initialized) + { + logDebug(hid) << "Cannot init receiver when initializing or already initialized."; + if (cb) cb(m_receiverState); + return; + } + + setReceiverState(ReceiverState::Initializing); + + if (m_busType != BusType::Usb) + { + // If bus type is not USB return immediately with success result and initialized state + setReceiverState(ReceiverState::Initialized); + if (cb) cb(m_receiverState); + return; + } + + using namespace HIDPP; + using Type = HIDPP::Message::Type; + + int index = -1; + + RequestBatch batch{{ + RequestBatchItem{ + // Reset device: get rid of any device configuration by other programs + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 0, {}), + makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb receiver init error; step %1: %2") + .arg(index).arg(toString(result)); + }) + }, + RequestBatchItem{ + // Turn off software bit and keep the wireless notification bit on + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, + {0x00, 0x01, 0x00}), + makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb receiver init error; step %1: %2") + .arg(index).arg(toString(result)); + }) + }, + RequestBatchItem{ + // Initialize USB dongle + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 2, {}), + makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb receiver init error; step %1: %2") + .arg(index).arg(toString(result)); + }) + }, + RequestBatchItem{ + // --- + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 2, + {0x02, 0x00, 0x00}), + makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb receiver init error; step %1: %2") + .arg(index).arg(toString(result)); + }) + }, + RequestBatchItem{ + // Now enable both software and wireless notification bit + Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, + {0x00, 0x09, 0x00}), + makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + if (result == MsgResult::Ok) return; + logWarn(hid) << tr("Usb receiver init error; step %1: %2") + .arg(index).arg(toString(result)); + }) + }, + }}; + + sendRequestBatch(std::move(batch), + makeSafeCallback([this, cb=std::move(cb)](std::vector results) + { + setReceiverState(results.back() == MsgResult::Ok ? ReceiverState::Initialized + : ReceiverState::Error); + if (cb) cb(m_receiverState); + })); }); } // ------------------------------------------------------------------------------------------------- -void SubHidppConnection::initialize() +void SubHidppConnection::initPresenter(std::function cb) { - if (!hasFlags(DeviceFlag::Hidpp)) return; + postSelf([this, cb=std::move(cb)](){ + if (m_presenterState == PresenterState::Initializing + || m_presenterState == PresenterState::Initialized_Offline + || m_presenterState == PresenterState::Initialized_Online + || m_presenterState == PresenterState::Uninitialized_Offline) + { + logDebug(hid) << "Cannot init presenter when offline, initializing or already initialized."; + if (cb) cb(m_presenterState); + return; + } + + setPresenterState(PresenterState::Initializing); + + m_featureSet.initFromDevice(makeSafeCallback( + [this, cb=std::move(cb)](HIDPP::FeatureSet::State state) + { + using FState = HIDPP::FeatureSet::State; + switch (state) + { + case FState::Error: { + setPresenterState(PresenterState::Error); + break; + } + case FState::Uninitialized: + case FState::Initializing: { + logError(hid) << tr("Unexpected state from feature set."); + setPresenterState(PresenterState::Error); + break; + } + case FState::Initialized: { + setPresenterState(PresenterState::Initialized_Online); + break; + } + } + if (cb) cb(m_presenterState); + })); + }); +} - // TODO set state: not_initialized, initializing, initialized for the subdevice +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::subDeviceInit() +{ + if (!hasFlags(DeviceFlag::Hidpp)) return; - initUsbReceiver(makeSafeCallback([this](MsgResult res) + // Init receiver - will return almost immediately for bluetooth connections + initReceiver(makeSafeCallback([this](ReceiverState rs) { - if (res != MsgResult::Ok) { - // TODO log error - re-schedule init? - } - m_featureSet.initFromDevice(); + Q_UNUSED(rs); + // Independent of the receiver init result, try to initialize the + // presenter device HID++ features and more + checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState ps) { + logDebug(hid) << tr("subDeviceInit, checkAndUpdatePresenterState = %1").arg(toString(ps)); + })); })); // DeviceFlags featureFlags = DeviceFlag::NoFlags; @@ -469,7 +605,7 @@ void SubHidppConnection::initialize() // TODO implement // Enable Next and back button on hold functionality. - const auto rcIndex = m_featureSet.getFeatureIndex(HIDPP::FeatureCode::ReprogramControlsV4); + const auto rcIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::ReprogramControlsV4); if (rcIndex) { // if (hasFlags(DeviceFlags::NextHold)) { // QTimer::singleShot(delay_ms*msgCount, this, [this, rcIndex](){ @@ -500,36 +636,121 @@ void SubHidppConnection::initialize() } // ------------------------------------------------------------------------------------------------- -void SubHidppConnection::pingSubDevice() { - // constexpr uint8_t rootIndex = 0x00; // root Index is always 0x00 in any logitech device - // const uint8_t pingCmd[] = {HIDPP::Bytes::SHORT_MSG, - // HIDPP::Bytes::MSG_TO_SPOTLIGHT, - // rootIndex, - // m_featureSet.getRandomFunctionCode(0x10), - // 0x00, - // 0x00, - // 0x5d}; - // sendData(pingCmd, sizeof(pingCmd)); +SubHidppConnection::ReceiverState SubHidppConnection::receiverState() const { + return m_receiverState; } -// // ------------------------------------------------------------------------------------------------- -// void SubHidppConnection::setHIDppProtocol(float version) { -// // Inform user about the online status of device. -// if (version > 0) { -// if (HIDppProtocolVer < 0) { -// logDebug(hid) << "HID++ Device with path" << path() << "is now active."; -// logDebug(hid) << "HID++ protocol version" << tr("%1.").arg(version); -// emit activated(); -// } -// } -// else { -// if (HIDppProtocolVer > 0) { -// logDebug(hid) << "HID++ Device with path" << path() << "got deactivated."; -// emit deactivated(); -// } -// } -// HIDppProtocolVer = version; -// } +// ------------------------------------------------------------------------------------------------- +SubHidppConnection::PresenterState SubHidppConnection::presenterState() const { + return m_presenterState; +} + +// ------------------------------------------------------------------------------------------------- +HIDPP::ProtocolVersion SubHidppConnection::protocolVersion() const { + return m_protocolVersion; +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::sendPing(RequestResultCallback cb) +{ + using namespace HIDPP; + // Ping wireless device 1 - same as requesting protocol version + Message pingMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, 0, 1, getRandomPingPayload()); + sendRequest(std::move(pingMsg), std::move(cb)); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::getProtocolVersion(std::function cb) +{ + sendPing([cb=std::move(cb)](MsgResult res, HIDPP::Message msg) { + if (cb) { + auto pv = (res == MsgResult::Ok) ? HIDPP::ProtocolVersion{ msg[4], msg[5] } + : HIDPP::ProtocolVersion(); + logDebug(hid) << tr("getProtocolVersion() => %1, version = %2.%3") + .arg(toString(res)).arg(pv.major).arg(pv.minor); + cb(res, (res == MsgResult::HidppError) ? msg.errorCode() + : HIDPP::Error::NoError, std::move(pv)); + } + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::checkPresenterOnline(std::function cb) +{ + getProtocolVersion( + [cb=std::move(cb)](MsgResult res, HIDPP::Error err, HIDPP::ProtocolVersion pv) { + if (!cb) return; + const bool deviceOnline = MsgResult::Ok == res && err == HIDPP::Error::NoError; + if (!deviceOnline && err != HIDPP::Error::Unsupported) { + // Unsupported is send as error if the device is offline + logWarn(hid) << tr("Unexpected error for offline device (%1, %2)") + .arg(toString(res)).arg(toString(err)); + } + cb(deviceOnline, std::move(pv)); + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::checkAndUpdatePresenterState(std::function cb) +{ + postSelf([this, cb=std::move(cb)]() + { + if (m_presenterState == PresenterState::Initializing) + { + if (cb) cb(m_presenterState); + return; + } + + checkPresenterOnline(makeSafeCallback( + [this, cb=std::move(cb)](bool isOnline, HIDPP::ProtocolVersion pv) + { + if (!isOnline) + { + switch (m_presenterState) + { + case PresenterState::Initialized_Online: // [[fallthrough]]; + case PresenterState::Initialized_Offline: { + setPresenterState(PresenterState::Initialized_Offline); + break; + } + case PresenterState::Error: break; + case PresenterState::Initializing: break; + case PresenterState::Uninitialized_Offline: // [[fallthrough]]; + case PresenterState::Uninitialized: { + setPresenterState(PresenterState::Uninitialized_Offline); + } + } + if (cb) cb(m_presenterState); + return; + } + + // device is online, set protocol version and init device feature table if necessary. + m_protocolVersion = std::move(pv); + + if (m_presenterState == PresenterState::Uninitialized + || m_presenterState == PresenterState::Uninitialized_Offline + || m_presenterState == PresenterState::Error) + { + if (m_protocolVersion.smallerThan(2, 0)) + { + logWarn(hid) << tr("Hid++ version < 2.0 not supported. (%1)").arg(path()); + setPresenterState(PresenterState::Error); + if (cb) cb(m_presenterState); + return; + } + + initPresenter(std::move(cb)); + } + else if (m_presenterState == PresenterState::Initialized_Offline + || m_presenterState == PresenterState::Initialized_Online) + { + setPresenterState(PresenterState::Initialized_Online); + if (cb) cb(m_presenterState); + } + })); + }); +} // ------------------------------------------------------------------------------------------------- void SubHidppConnection::onHidppDataAvailable(int fd) @@ -544,8 +765,9 @@ void SubHidppConnection::onHidppDataAvailable(int fd) } if (!msg.isValid()) { + // TODO check for move pointer messages on hid++ device, else log invalid message logDebug(hid) << tr("Received invalid HID++ message " - "'%1' from %2").arg(qPrintable(msg.hex()), path()); + "'%1' from %2").arg(msg.hex(), path()); return; } diff --git a/src/device-hidpp.h b/src/device-hidpp.h index 92f600b8..8e3aecd0 100644 --- a/src/device-hidpp.h +++ b/src/device-hidpp.h @@ -11,13 +11,23 @@ class QTimer; // ------------------------------------------------------------------------------------------------- -/// @brief TODO +/// Hid++ connection class class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInterface { Q_OBJECT public: - enum class State : uint8_t {Uninitialized, Initializing, Initialized, Error}; + /// Initialization state of the Usb dongle - for bluetooth this will be always initialized. + enum class ReceiverState : uint8_t { Uninitialized, Initializing, Initialized, Error }; + /// Initialization state of the wireless presenter. + /// * Uninitialized - no information had been collected and no defaults had been set up + /// * Uninitialized_Offline - same as above, but online check detected offline device + /// * Initializing - currently fetching feature sets and setting defaults and other information + /// * Initialized_Online - device initialized and online + /// * Initialized_Offline - device initialized but offline (only relevant when using usb dongle) + /// * Error - An error occured during initialization. + enum class PresenterState : uint8_t { Uninitialized, Uninitialized_Offline, Initializing, + Initialized_Online, Initialized_Offline, Error }; static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); @@ -42,27 +52,39 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, bool continueOnError = false) override; - // ------- + // --- + + PresenterState presenterState() const; + ReceiverState receiverState() const; + + void sendPing(RequestResultCallback cb); + HIDPP::ProtocolVersion protocolVersion() const; void queryBatteryStatus() override; void sendVibrateCommand(uint8_t intensity, uint8_t length) override; - void pingSubDevice(); void setPointerSpeed(uint8_t level); - // void setHIDppProtocol(float version); - // float getHIDppProtocol() const override { return HIDppProtocolVer; }; - // bool isOnline() const override { return (HIDppProtocolVer > 0); }; - - void initialize(); signals: + void receiverStateChanged(ReceiverState); + void presenterStateChanged(PresenterState); + void receivedBatteryInfo(QByteArray batteryData); - void activated(); - void deactivated(); private: + void subDeviceInit(); + void initReceiver(std::function); + void initPresenter(std::function); + + void setReceiverState(ReceiverState rs); + void setPresenterState(PresenterState ps); + void onHidppDataAvailable(int fd); + + void getProtocolVersion(std::function cb); + void checkPresenterOnline(std::function cb); + void checkAndUpdatePresenterState(std::function cb); + void clearTimedOutRequests(); - void initUsbReceiver(std::function); void sendDataBatch(DataBatch requestBatch, DataBatchResultCallback cb, bool continueOnError, std::vector results); @@ -71,7 +93,12 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt HIDPP::FeatureSet m_featureSet; const BusType m_busType = BusType::Unknown; + HIDPP::ProtocolVersion m_protocolVersion; + ReceiverState m_receiverState = ReceiverState::Uninitialized; + PresenterState m_presenterState = PresenterState::Uninitialized; + + /// A request entry for request messages sent to the device. struct RequestEntry { HIDPP::Message request; // bytes 0(or 1) to 5 should be enough to check against reply std::chrono::time_point validUntil; @@ -81,3 +108,6 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt std::list m_requests; QTimer* m_requestCleanupTimer = nullptr; }; + +const char* toString(SubHidppConnection::ReceiverState rs); +const char* toString(SubHidppConnection::PresenterState ps); diff --git a/src/device.cc b/src/device.cc index 45b210fc..05391d94 100644 --- a/src/device.cc +++ b/src/device.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "device.h" #include "deviceinput.h" @@ -86,7 +88,8 @@ void DeviceConnection::queryBatteryStatus() { for (const auto& sd: subDevices()) { - if (sd.second->type() == ConnectionType::Hidraw && sd.second->mode() == ConnectionMode::ReadWrite) + if (sd.second->type() == ConnectionType::Hidraw + && sd.second->mode() == ConnectionMode::ReadWrite) { if (sd.second->hasFlags(DeviceFlag::ReportBattery)) sd.second->queryBatteryStatus(); } @@ -96,19 +99,19 @@ void DeviceConnection::queryBatteryStatus() // ------------------------------------------------------------------------------------------------- void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) { - const bool hasBattery = std::any_of(m_subDeviceConnections.cbegin(), m_subDeviceConnections.cend(), - [](const auto& sd) - { - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - sd.second->hasFlags(DeviceFlag::ReportBattery)); - }); + // TODO Remove / refactor with hid++ update 2 + const bool hasBattery = + std::any_of(m_subDeviceConnections.cbegin(), m_subDeviceConnections.cend(), [](const auto& sd) { + return sd.second->hasFlags(DeviceFlag::ReportBattery); + }); if (hasBattery && batteryData.length() == 3) { // Battery percent is only meaningful when battery is discharging. However, save them anyway. - m_batteryInfo.currentLevel = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0): 100); - m_batteryInfo.nextReportedLevel = static_cast(batteryData.at(1) <= 100 ? batteryData.at(1): 100); + m_batteryInfo.currentLevel + = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0) : 100); + m_batteryInfo.nextReportedLevel + = static_cast(batteryData.at(1) <= 100 ? batteryData.at(1): 100); m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x07) ? batteryData.at(2): 0x07); } } diff --git a/src/device.h b/src/device.h index 981e41e9..8a1c81e1 100644 --- a/src/device.h +++ b/src/device.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include "asynchronous.h" @@ -49,7 +50,7 @@ struct BatteryInfo // ------------------------------------------------------------------------------------------------- -/// @brief TODO +/// The main device connection class, which usually consists of one or multiple sub devices. class DeviceConnection : public QObject { Q_OBJECT diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 492a93aa..83106847 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #include "deviceinput.h" #include "logging.h" diff --git a/src/deviceinput.h b/src/deviceinput.h index 814aa0d6..da43811c 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/devicescan.cc b/src/devicescan.cc index 4782010f..4b50ce75 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "devicescan.h" #include diff --git a/src/devicescan.h b/src/devicescan.h index 6f81d86c..a69df02f 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include "device-defs.h" diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index e9cacd33..05c5a4bc 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "deviceswidget.h" #include "device.h" diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 55144e36..2c9ab651 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include "device.h" diff --git a/src/enum-helper.h b/src/enum-helper.h index 389748f4..8c5ff3f1 100644 --- a/src/enum-helper.h +++ b/src/enum-helper.h @@ -1,15 +1,16 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include -/// @brief Cast enum type to underlying integral type. +/// Cast enum type to underlying integral type. template constexpr auto to_integral(T e) { return static_cast>(e); } -/// @brief Cast integral type to a given enum type. +/// Cast integral type to a given enum type. template constexpr auto to_enum(T v) { return static_cast(v); @@ -36,3 +37,5 @@ constexpr auto to_enum(T v) { #define ENUM2(ENUMCLASS, PLURALNAME) \ ENUM1(ENUMCLASS); \ using PLURALNAME = ENUMCLASS; + +#define ENUM_CASE_STRINGIFY(x) case x: return #x diff --git a/src/extra-devices.cc.in b/src/extra-devices.cc.in index f775a54e..a69d3a8b 100644 --- a/src/extra-devices.cc.in +++ b/src/extra-devices.cc.in @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #include "devicescan.h" #include diff --git a/src/hidpp.cc b/src/hidpp.cc index 0e23065f..f69c5d30 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -1,25 +1,24 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "hidpp.h" #include "logging.h" #include "enum-helper.h" #include +#include #include - DECLARE_LOGGING_CATEGORY(hid) -#define STRINGIFY(x) #x -#define ENUM_CASE_STRINGIFY(x) case x: return STRINGIFY(x) - -// ------------------------------------------------------------------------------------------------- namespace { - + // ----------------------------------------------------------------------------------------------- namespace Defaults { constexpr uint8_t HidppSoftwareId = 7; } + // ----------------------------------------------------------------------------------------------- // -- HID++ message offsets namespace Offset { constexpr uint32_t Type = 0; @@ -32,32 +31,39 @@ namespace { constexpr uint32_t ErrorFeatureIndex = ErrorSubId; constexpr uint32_t ErrorAddress = 4; constexpr uint32_t ErrorCode = 5; + + constexpr uint32_t Payload = 4; + + constexpr uint32_t FwType = Payload; + constexpr uint32_t FwPrefix = FwType + 1; + constexpr uint32_t FwVersion = FwPrefix + 3; + constexpr uint32_t FwBuild = FwVersion + 2; } + // ----------------------------------------------------------------------------------------------- namespace Defines { constexpr uint8_t ErrorShort = 0x8f; constexpr uint8_t ErrorLong = 0xff; } + // ----------------------------------------------------------------------------------------------- uint8_t funcSwIdToByte(uint8_t function, uint8_t swId) { return (swId & 0x0f)|((function & 0x0f) << 4); } + // ----------------------------------------------------------------------------------------------- uint8_t getRandomByte() { static std::mt19937 gen(std::random_device{}()); std::uniform_int_distribution distribution; return distribution(gen); } - - HIDPP::Message::Data getRandomPingPayload() { - return {0, 0, getRandomByte()}; - } - } // end anonymous namespace -const char* HidppConnectionInterface::toString(MsgResult res) +// ------------------------------------------------------------------------------------------------- +const char* toString(HidppConnectionInterface::MsgResult res) { + using MsgResult = HidppConnectionInterface::MsgResult; switch(res) { ENUM_CASE_STRINGIFY(MsgResult::Ok); ENUM_CASE_STRINGIFY(MsgResult::InvalidFormat); @@ -68,11 +74,10 @@ const char* HidppConnectionInterface::toString(MsgResult res) return "MsgResult::(unknown)"; } -namespace HIDPP { // ------------------------------------------------------------------------------------------------- - -const char* toString(Error e) +const char* toString(HIDPP::Error e) { + using Error = HIDPP::Error; switch(e) { ENUM_CASE_STRINGIFY(Error::NoError); ENUM_CASE_STRINGIFY(Error::Unknown); @@ -88,16 +93,24 @@ const char* toString(Error e) return "Error::(unknown)"; } +namespace HIDPP { // ------------------------------------------------------------------------------------------------- +Message::Data getRandomPingPayload() { + return {0, 0, getRandomByte()}; +} +// ------------------------------------------------------------------------------------------------- Message::Message() = default; +// ------------------------------------------------------------------------------------------------- Message::Message(Type type) : Message(type, DeviceIndex::DefaultDevice, 0, 0, Defaults::HidppSoftwareId, {}) {} +// ------------------------------------------------------------------------------------------------- Message::Message(std::vector&& data) : m_data(std::move(data)) {} +// ------------------------------------------------------------------------------------------------- Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, uint8_t swId, Data payload) : Message(Data{to_integral(type), deviceIndex, featureIndex, funcSwIdToByte(function, swId)}) @@ -111,11 +124,23 @@ Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t f else if (type == Type::Short) m_data.resize(7, 0x0); } +// ------------------------------------------------------------------------------------------------- Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, Data payload) : Message(type, deviceIndex, featureIndex, function, Defaults::HidppSoftwareId, std::move(payload)) {} +// ------------------------------------------------------------------------------------------------- +Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, Data payload) + : Message(type, deviceIndex, featureIndex, 0, Defaults::HidppSoftwareId, std::move(payload)) +{} + +// ------------------------------------------------------------------------------------------------- +Message::Message(Type type, uint8_t deviceIndex, Data payload) + : Message(type, deviceIndex, 0, 0, Defaults::HidppSoftwareId, std::move(payload)) +{} + +// ------------------------------------------------------------------------------------------------- size_t Message::size() const { if (isLong()) return 20; @@ -123,6 +148,7 @@ size_t Message::size() const return 0; } +// ------------------------------------------------------------------------------------------------- Message::Type Message::type() const { if (isLong()) return Type::Long; @@ -130,16 +156,20 @@ Message::Type Message::type() const return Type::Invalid; } +// ------------------------------------------------------------------------------------------------- bool Message::isValid() const { return isLong() || isShort(); } +// ------------------------------------------------------------------------------------------------- bool Message::isShort() const { return (m_data.size() >= 7 && m_data[Offset::Type] == to_integral(Message::Type::Short)); } +// ------------------------------------------------------------------------------------------------- bool Message::isLong() const { return (m_data.size() >= 20 && m_data[Offset::Type] == to_integral(Message::Type::Long)); } +// ------------------------------------------------------------------------------------------------- bool Message::isError() const { if (isShort() && m_data[Offset::SubId] == Defines::ErrorShort) { @@ -151,74 +181,92 @@ bool Message::isError() const return false; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::errorSubId() const { return m_data[Offset::ErrorSubId]; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::errorAddress() const { return m_data[Offset::ErrorAddress]; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::errorFeatureIndex() const { return m_data[Offset::ErrorFeatureIndex]; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::errorFunction() const { return ((m_data[Offset::ErrorAddress] & 0xf0) >> 4); } +// ------------------------------------------------------------------------------------------------- uint8_t Message::errorSoftwareId() const { return (m_data[Offset::ErrorAddress] & 0x0f); } +// ------------------------------------------------------------------------------------------------- HIDPP::Error Message::errorCode() const { return to_enum(m_data[Offset::ErrorCode]); } +// ------------------------------------------------------------------------------------------------- uint8_t Message::deviceIndex() const { return m_data[Offset::DeviceIndex]; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::subId() const { return m_data[Offset::SubId]; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::address() const { return m_data[Offset::Address]; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::featureIndex() const { return m_data[Offset::Address]; } +// ------------------------------------------------------------------------------------------------- uint8_t Message::function() const { return ((m_data[Offset::Address] & 0xf0) >> 4); } +// ------------------------------------------------------------------------------------------------- uint8_t Message::softwareId() const { return (m_data[Offset::Address] & 0x0f); } +// ------------------------------------------------------------------------------------------------- void Message::setSubId(uint8_t subId) { m_data[Offset::SubId] = subId; } +// ------------------------------------------------------------------------------------------------- void Message::setAddress(uint8_t address) { m_data[Offset::Address] = address; } +// ------------------------------------------------------------------------------------------------- void Message::setFeatureIndex(uint8_t featureIndex) { m_data[Offset::FeatureIndex] = featureIndex; } +// ------------------------------------------------------------------------------------------------- void Message::setFunction(uint8_t function) { m_data[Offset::Address] = ((function & 0x0f) << 4) | (m_data[Offset::Address] & 0x0f); } +// ------------------------------------------------------------------------------------------------- void Message::setSoftwareId(uint8_t softwareId) { m_data[Offset::Address] = (softwareId & 0x0f) | (m_data[Offset::Address] & 0xf0); } +// ------------------------------------------------------------------------------------------------- bool Message::isResponseTo(const Message& other) const { if (!isValid() || !other.isValid()) return false; @@ -228,6 +276,7 @@ bool Message::isResponseTo(const Message& other) const && address() == other.address(); } +// ------------------------------------------------------------------------------------------------- bool Message::isErrorResponseTo(const Message& other) const { if (!isValid() || !other.isValid()) return false; @@ -237,317 +286,376 @@ bool Message::isErrorResponseTo(const Message& other) const && errorAddress() == other.address(); } +// ------------------------------------------------------------------------------------------------- Message& Message::convertToLong() { if (!isShort()) return *this; + // Resize data vector, pad with zeroes. m_data.resize(20, 0); m_data[Offset::Type] = to_integral(Type::Long); return *this; } +// ------------------------------------------------------------------------------------------------- Message Message::toLong() const { return Message(*this).convertToLong(); } +// ------------------------------------------------------------------------------------------------- QString Message::hex() const { return qPrintable(QByteArray::fromRawData( - reinterpret_cast(m_data.data()), size()).toHex() + reinterpret_cast(m_data.data()), isValid() ? size() : m_data.size()).toHex() ); } -// ------------------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------------------- - +// ================================================================================================= FeatureSet::FeatureSet(HidppConnectionInterface* connection, QObject* parent) : QObject(parent) , m_connection(connection) {} // ------------------------------------------------------------------------------------------------- +FeatureSet::State FeatureSet::state() const { + return m_state; +} -void FeatureSet::getProtocolVersion(std::function cb) +// ------------------------------------------------------------------------------------------------- +void FeatureSet::setState(State s) { - if (!m_connection) { - if (cb) cb(MsgResult::WriteError, Error::Unknown, {}); - return; - } - - // Get wireless device 1 protocol version - Message reqMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, 0, 1, getRandomPingPayload()); + if (s == m_state) return; - m_connection->sendRequest(std::move(reqMsg), - makeSafeCallback([this, cb=std::move(cb)](MsgResult res, Message msg) { - if (cb) { - auto pv = (res == MsgResult::Ok) ? ProtocolVersion{ msg[4], msg[5] } : ProtocolVersion(); - cb(res, (res == MsgResult::HidppError) ? msg.errorCode() : Error::NoError, std::move(pv)); - } - })); + m_state = s; + emit stateChanged(m_state); } +// ------------------------------------------------------------------------------------------------- void FeatureSet::getFeatureIndex(FeatureCode fc, std::function cb) { postSelf([this, fc, cb=std::move(cb)]() { + if (m_connection == nullptr) + { + if (cb) cb(MsgResult::WriteError, 0); + return; + } + + const uint8_t fcLSB = static_cast(to_integral(fc) >> 8); + const uint8_t fcMSB = static_cast(to_integral(fc) & 0x00ff); + + Message featureIndexReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, + Message::Data{fcLSB, fcMSB}); + m_connection->sendRequest(std::move(featureIndexReqMsg), + [cb=std::move(cb), fc, fcLSB, fcMSB](MsgResult result, Message msg) + { + logDebug(hid) << tr("getFeatureIndex(%1) => %2, %3") + .arg(to_integral(fc)).arg(toString(result)).arg(msg[4]); + if (cb) cb(result, (result != MsgResult::Ok) ? 0 : msg[4]); + }); }); } -void FeatureSet::initFromDevice() +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFeatureCount(std::function cb) { - if (m_connection == nullptr) return; - - getProtocolVersion(makeSafeCallback([this](MsgResult res, Error err, ProtocolVersion pv) + getFeatureIndex(FeatureCode::FeatureSet, makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) { - if (err == Error::NoError) {} - logDebug(hid) << tr("ProtocolVersion = %2.%3 (%1)") - .arg(m_connection->toString(res)) - .arg(int(pv.major)) - .arg(int(pv.minor)); - m_protocolVersion = std::move(pv); + if (res != MsgResult::Ok) + { + if (cb) cb(res, 0, 0); + return; + } + + Message featureCountReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, featureIndex); + + m_connection->sendRequest(std::move(featureCountReqMsg), + [featureIndex, cb=std::move(cb)](MsgResult result, Message msg) { + if (cb) cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); + }); })); } // ------------------------------------------------------------------------------------------------- +void FeatureSet::getFirmwareCount(std::function cb) +{ + getFeatureIndex(FeatureCode::FirmwareVersion, makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) + { + if (res != MsgResult::Ok) + { + if (cb) cb(res, 0, 0); + return; + } + Message fwCountReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, featureIndex); -FeatureSet::State FeatureSet::state() const { - return m_state; + m_connection->sendRequest(std::move(fwCountReqMsg), + [featureIndex, cb=std::move(cb)](MsgResult result, Message msg) + { + logDebug(hid) << tr("getFirmwareCount() => %1, featureIndex = %2, count = %3") + .arg(toString(result)).arg(featureIndex).arg(msg[4]); + if (cb) cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); + }); + })); } -// // ------------------------------------------------------------------------------------------------- -// QByteArray FeatureSet::getResponseFromDevice(const QByteArray& expectedBytes) -// { -// if (m_connection == nullptr) return QByteArray(); - -// QByteArray readVal(20, 0); -// int timeOut = 4; // time out just in case device did not reply; -// // 4 seconds time out is used by other programs like Solaar. -// QTime timeOutTime = QTime::currentTime().addSecs(timeOut); -// while(true) { -// if(::read(m_fdHIDDevice, readVal.data(), readVal.length())) { -// if (readVal.mid(1, 3) == expectedBytes) return readVal; -// if (static_cast(readVal.at(2)) == 0x8f) return readVal; //Device not online -// if (QTime::currentTime() >= timeOutTime) return QByteArray(); -// } -// } -// } - -uint8_t FeatureSet::getFeatureIndexFromDevice(FeatureCode /* fc */) +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFirmwareInfo(uint8_t fwIndex, uint8_t entity, + std::function cb) { - if (m_connection == nullptr) return 0x00; - // TODO implement - - // using MsgResult = HidppConnectionInterface::MsgResult; - // const uint8_t fSetLSB = static_cast(to_integral(fc) >> 8); - // const uint8_t fSetMSB = static_cast(to_integral(fc)); - - // Message::Data featureReqMessage { - // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, 0x00, getRandomFunctionCode(0x00), - // fSetLSB, fSetMSB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - // }; - - // m_connection->sendRequest(std::move(featureReqMessage), - // makeSafeCallback([this](MsgResult result, Message msg) { - // if (result != MsgResult::Ok) { - // logDebug(hid) << tr("Failed to write feature request message to device."); - // } - // Q_UNUSED(msg); - // // TODO ?? getFeatureIndex with cb?? - // })); - - // const auto res = ::write(m_fdHIDDevice, featureReqMessage.data(), featureReqMessage.size()); - // if (res != featureReqMessage.size()) - // { - // logDebug(hid) << Hid_::tr("Failed to write feature request message to device."); - // return 0x00; - // } + if (m_connection == nullptr) + { + if (cb) cb(MsgResult::WriteError, FirmwareInfo()); + return; + } - // const auto response = getResponseFromDevice(featureReqMessage.mid(1, 3)); - // if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; - // uint8_t featureIndex = static_cast(response.at(4)); - // TODO + Message fwVerReqMessage(Message::Type::Long, DeviceIndex::WirelessDevice1, fwIndex, 1, + Message::Data{entity}); - return 0x00; //featureIndex; + m_connection->sendRequest(std::move(fwVerReqMessage), + [cb=std::move(cb)](MsgResult res, Message msg) { + if (cb) cb(res, FirmwareInfo(std::move(msg))); + }); } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureCountFromDevice(uint8_t /* featureSetIndex */) +void FeatureSet::getMainFirmwareInfo(std::function cb) { - if (m_connection == nullptr) return 0x00; - - // Get Number of features (except Root Feature) supported - // const auto featureCountReqMessage = make_QByteArray(HidppMsg{ - // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x00), - // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - // }); - // TODO implement - - // const auto res = ::write(m_fdHIDDevice, featureCountReqMessage.data(), featureCountReqMessage.size()); - // if (res != featureCountReqMessage.size()) - // { - // logDebug(hid) << Hid_::tr("Failed to write feature count request message to device."); - // return 0x00; - // } - - // const auto response = getResponseFromDevice(featureCountReqMessage.mid(1, 3)); - // if (!response.length() || static_cast(response.at(2)) == 0x8f) return 0x00; - // uint8_t featureCount = static_cast(response.at(4)); - - // TODO return featureCount; - return 0; + getFirmwareCount(makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) + { + if (res != MsgResult::Ok) + { + if (cb) cb(res, FirmwareInfo()); + return; + } + getMainFirmwareInfo(featureIndex, count, 0, std::move(cb)); + })); } -QByteArray FeatureSet::getFirmwareVersionFromDevice() +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t current, + std::function cb) { - if (m_connection == nullptr) return 0x00; - - // To get firmware details: first get Feature Index corresponding to Firmware feature code - uint8_t fwIndex = getFeatureIndexFromDevice(FeatureCode::FirmwareVersion); - if (!fwIndex) return QByteArray(); - - // Get the number of firmwares (Main HID++ application, BootLoader, or Hardware) now - // const auto fwCountReqMessage = make_QByteArray(HidppMsg{ - // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x00), - // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - // }); - - // TODO implement - // const auto res = ::write(m_fdHIDDevice, fwCountReqMessage.data(), fwCountReqMessage.size()); - // if (res != fwCountReqMessage.size()) - // { - // logDebug(hid) << Hid_::tr("Failed to write firmware count request message to device."); - // return 0x00; - // } - - // TODO implement - // const auto response = getResponseFromDevice(fwCountReqMessage.mid(1, 3)); - // if (!response.length() || static_cast(response.at(2)) == 0x8f) return QByteArray(); - // const uint8_t fwCount = static_cast(response.at(4)); - const uint8_t fwCount = 1; - - // TODO implement - // The following info is not used currently; however, these commented lines are kept for future reference. - // uint8_t connectionMode = static_cast(response.at(10)); - // bool supportBluetooth = (connectionMode & 0x01); - // bool supportBluetoothLE = (connectionMode & 0x02); // true for Logitech Spotlight - // bool supportUsbReceiver = (connectionMode & 0x04); // true for Logitech Spotlight - // bool supportUsbWired = (connectionMode & 0x08); - // auto unitID = response.mid(5, 4); - // auto modelIDs = response.mid(11, 8); - // int count = 0; - // if (supportBluetooth) { auto btmodelID = modelIDs.mid(count, 2); count += 2;} - // if (supportBluetoothLE) { auto btlemodelID = modelIDs.mid(count, 2); count += 2;} - // if (supportUsbReceiver) { auto wpmodelID = modelIDs.mid(count, 2); count += 2;} - // if (supportUsbWired) { auto usbmodelID = modelIDs.mid(count, 2); count += 2;} - - // TODO implement - // Iteratively find out firmware versions for all firmwares and get the firmware for main application - for (uint8_t i = 0x00; i < fwCount; i++) + getFirmwareInfo(fwIndex, current, makeSafeCallback( + [this, current, max, fwIndex, cb=std::move(cb)](MsgResult res, FirmwareInfo fi) { - // const auto fwVerReqMessage = make_QByteArray(HidppMsg{ - // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, fwIndex, getRandomFunctionCode(0x10), - // i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - // }); - - // const auto res = ::write(m_fdHIDDevice, fwVerReqMessage.data(), fwVerReqMessage.length()); - // if (res != fwCountReqMessage.size()) - // { - // logDebug(hid) << Hid_::tr("Failed to write firmware request message to device (%1).") - // .arg(int(i)); - // return 0x00; - // } - // const auto fwResponse = getResponseFromDevice(fwVerReqMessage.mid(1, 3)); - // if (!fwResponse.length() || static_cast(fwResponse.at(2)) == 0x8f) return QByteArray(); - // const auto fwType = (fwResponse.at(4) & 0x0f); // 0 for main HID++ application, 1 for BootLoader, 2 for Hardware, 3-15 others - // const auto fwVersion = fwResponse.mid(5, 7); - // // Currently we are not interested in these details; however, these commented lines are kept for future reference. - // //auto firmwareName = fwVersion.mid(0, 3).data(); - // //auto majorVesion = fwResponse.at(3); - // //auto MinorVersion = fwResponse.at(4); - // //auto build = fwResponse.mid(5); - // if (fwType == 0) - // { - // logDebug(hid) << "Main application firmware Version:" << fwVersion.toHex(); - // return fwVersion; - // } - } - return QByteArray(); + logDebug(hid) << tr("getFirmwareInfo(%1, %2, %3) => %4, fi.type = %5, fi.ver = %6, fi.pref = %7") + .arg(fwIndex).arg(max).arg(current).arg(toString(res)) + .arg(to_integral(fi.firmwareType())).arg(fi.firmwareVersion()).arg(fi.firmwarePrefix()); + + if (res == MsgResult::Ok && fi.firmwareType() == FirmwareInfo::FirmwareType::MainApp) + { + if (cb) cb(res, std::move(fi)); + return; + } + + if (max == current + 1) { + if (cb) cb(res, FirmwareInfo()); + return; + } + + getMainFirmwareInfo(fwIndex, max, current + 1, std::move(cb)); + })); } // ------------------------------------------------------------------------------------------------- -void FeatureSet::populateFeatureTable() +void FeatureSet::initFromDevice(std::function cb) { - if (m_connection == nullptr) return; + postSelf([this, cb=std::move(cb)]() + { + if (m_connection == nullptr || m_state == State::Initialized || m_state == State::Initializing) + { + if (cb) cb(m_state); + return; + } - // Get the firmware version - const auto firmwareVersion = getFirmwareVersionFromDevice(); - if (!firmwareVersion.length()) return; + setState(State::Initializing); - // TODO:: Read and write cache file (settings most probably) - // if the firmware details match with cached file; then load the FeatureTable from file - // else read the entire feature table from the device - QByteArray cacheFirmwareVersion; // currently a dummy variable for Firmware Version from cache file. + getMainFirmwareInfo(makeSafeCallback([this, cb=std::move(cb)](MsgResult res, FirmwareInfo fi) + { + logDebug(hid) << tr("getMainFirmwareInfo() => %1, fi.type = %2").arg(toString(res)) + .arg(to_integral(fi.firmwareType())); + + if (fi.firmwareType() == FirmwareInfo::FirmwareType::MainApp) { + m_mainFirmwareInfo = std::move(fi); + } + + // Independent from firmware result try to get features + Q_UNUSED(res); + + getFeatureCount(makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) + { + logDebug(hid) << tr("getFeatureCount() => %1, featureIndex = %2, count = %3") + .arg(toString(res)).arg(featureIndex).arg(count); + + if (res != MsgResult::Ok) + { + setState(State::Error); + if (cb) cb(m_state); + return; + } + + getFeatureIds(featureIndex, count, makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, FeatureTable ft) + { + if (res != MsgResult::Ok) { + setState(State::Error); + } + else + { + m_featureTable = std::move(ft); + setState(State::Initialized); + } + + if (cb) cb(m_state); + })); // getFeatureIds (table) + })); // getFeatureCount + })); // getMainFwInfo + }); // postSelf +} - if (firmwareVersion == cacheFirmwareVersion) +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, + std::function cb) +{ + if (m_connection == nullptr) { - // TODO: load the featureSet from the cache file + if (cb) cb(MsgResult::WriteError, FeatureTable{}); // empty featuretable + return; } - else + + if (count == 0) { - // For reading feature table from device - // first get featureIndex for FeatureCode::FeatureSet - // then we can get the number of features supported by the device (except Root Feature) - const uint8_t featureSetIndex = getFeatureIndexFromDevice(FeatureCode::FeatureSet); - if (!featureSetIndex) return; - const uint8_t featureCount = getFeatureCountFromDevice(featureSetIndex); - if (!featureCount) return; - - // Root feature is supported by all HID++ 2.0 device and has a featureIndex of 0 always. - m_featureTable.insert({to_integral(FeatureCode::Root), 0x00}); - - // Read Feature Code for other featureIndices from device. - for (uint8_t featureIndex = 0x01; featureIndex <= featureCount; ++featureIndex) - { - // const auto featureCodeReqMsg = make_QByteArray(HidppMsg{ - // HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, featureSetIndex, getRandomFunctionCode(0x10), featureIndex, - // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - // }); - // const auto res = ::write(m_fdHIDDevice, featureCodeReqMsg.data(), featureCodeReqMsg.size()); - // if (res != featureCodeReqMsg.size()) { - // logDebug(hid) << Hid_::tr("Failed to write feature code request message to device."); - // return; - // } - - // const auto response = getResponseFromDevice(featureCodeReqMsg.mid(1, 3)); - // if (!response.length() || static_cast(response.at(2)) == 0x8f) { - // m_featureTable.clear(); - // return; - // } - // const uint16_t featureCode = (static_cast(response.at(4)) << 8) | static_cast(response.at(5)); - // const uint8_t featureType = static_cast(response.at(6)); - // const auto softwareHidden = (featureType & (1<<6)); - // const auto obsoleteFeature = (featureType & (1<<7)); - // if (!(softwareHidden) && !(obsoleteFeature)) m_featureTable.insert({featureCode, featureIndex}); - } + if (cb) cb(MsgResult::Ok, FeatureTable{}); // no count, empty featuretable + return; } + + auto featureTable = std::make_shared(); + + HidppConnectionInterface::RequestBatch batch; + for (uint8_t featureIndex = 1; featureIndex <= count; ++featureIndex) + { + // featureIdReqMsg[Offset::Payload] = featureIndex; + batch.emplace( + HidppConnectionInterface::RequestBatchItem { + Message(Message::Type::Long, DeviceIndex::WirelessDevice1, featureSetIndex, 1, + Message::Data{featureIndex}), + makeSafeCallback([featureTable, featureIndex](MsgResult res, Message msg) + { + if (res != MsgResult::Ok) return; + const uint16_t featureCode = (static_cast(msg[4]) << 8) + | static_cast(msg[5]); + const uint8_t featureType = msg[6]; + const bool softwareHidden = (featureType & (1<<6)); + const bool obsoleteFeature = (featureType & (1<<7)); + if (!softwareHidden && !obsoleteFeature) { + // logDebug(hid) << tr("featureCode %1 -> index %2").arg(featureCode).arg(featureIndex); + featureTable->emplace(featureCode, featureIndex); + } + })} + ); + } + + m_connection->sendRequestBatch(std::move(batch), + [featureTable, cb=std::move(cb)](std::vector results) + { + + if (cb) cb(results.back(), std::move(*featureTable)); + }); } // ------------------------------------------------------------------------------------------------- -bool FeatureSet::supportFeatureCode(FeatureCode fc) const +bool FeatureSet::featureCodeSupported(FeatureCode fc) const { const auto featurePair = m_featureTable.find(to_integral(fc)); return (featurePair != m_featureTable.end()); } // ------------------------------------------------------------------------------------------------- -uint8_t FeatureSet::getFeatureIndex(FeatureCode fc) const +uint8_t FeatureSet::featureIndex(FeatureCode fc) const { - if (!supportFeatureCode(fc)) return 0x00; + if (!featureCodeSupported(fc)) return 0x00; const auto featureInfo = m_featureTable.find(to_integral(fc)); return featureInfo->second; } +// ================================================================================================= +FirmwareInfo::FirmwareInfo(Message&& msg) + : m_rawMsg(std::move(msg)) +{} + +// ------------------------------------------------------------------------------------------------- +FirmwareInfo::FirmwareType FirmwareInfo::firmwareType() const +{ + if (!m_rawMsg.isLong()) return FirmwareType::Invalid; + + switch(m_rawMsg[Offset::Payload] & 0xf) + { + case 0: return FirmwareType::MainApp; + case 1: return FirmwareType::Bootloader; + case 2: return FirmwareType::Hardware; + default: return FirmwareType::Other; + } +} + +// ------------------------------------------------------------------------------------------------- +QString FirmwareInfo::firmwarePrefix() const +{ + if (!m_rawMsg.isLong()) return QString(); + + return QString( + QByteArray::fromRawData(reinterpret_cast(&m_rawMsg[Offset::FwPrefix]), 3) + ); +} + +// ------------------------------------------------------------------------------------------------- +uint16_t FirmwareInfo::firmwareVersion() const +{ + if (!m_rawMsg.isLong()) return 0; + + const auto& fwVersionMsb = m_rawMsg[Offset::FwVersion]; + const auto& fwVersionLsb = m_rawMsg[Offset::FwVersion+1]; + + // Firmware version is BCD encoded + return ( fwVersionLsb & 0xF) + + (((fwVersionLsb >> 4 ) & 0xF) * 10) + + (( fwVersionMsb & 0xF) * 100) + + (((fwVersionMsb >> 4) & 0xF) * 1000); +} + +// ------------------------------------------------------------------------------------------------- +uint16_t FirmwareInfo::firmwareBuild() const +{ + if (!m_rawMsg.isLong()) return 0; + + const auto& fwBuildMsb = m_rawMsg[Offset::FwBuild]; + const auto& fwBuildLsb = m_rawMsg[Offset::FwBuild+1]; + + // Firmware build is BCD encoded ?? + return ( fwBuildLsb & 0xF) + + (((fwBuildLsb >> 4 ) & 0xF) * 10) + + (( fwBuildMsb & 0xF) * 100) + + (((fwBuildMsb >> 4) & 0xF) * 1000); +} + } // end namespace HIDPP + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::FeatureSet::State s) +{ + using State = HIDPP::FeatureSet::State; + switch (s) + { + ENUM_CASE_STRINGIFY(State::Uninitialized); + ENUM_CASE_STRINGIFY(State::Initialized); + ENUM_CASE_STRINGIFY(State::Initializing); + ENUM_CASE_STRINGIFY(State::Error); + }; + return "State::(unknown)"; +} diff --git a/src/hidpp.h b/src/hidpp.h index 65e3d78c..0b1e7eb5 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -6,6 +6,7 @@ #include "device-defs.h" #include "asynchronous.h" +#include #include #include #include @@ -18,6 +19,7 @@ // - also see https://github.com/cvuchener/g500/blob/master/doc/hidpp10.md namespace HIDPP { + // ----------------------------------------------------------------------------------------------- namespace DeviceIndex { constexpr uint8_t DefaultDevice = 0xff; constexpr uint8_t CordedDevice = 0x00; @@ -29,6 +31,7 @@ namespace HIDPP { constexpr uint8_t WirelessDevice6 = 6; } // end namespace DeviceIndex + // ----------------------------------------------------------------------------------------------- // see also: https://github.com/cvuchener/hidpp/blob/master/src/tools/hidpp-list-features.cpp // Feature Codes important for Logitech Spotlight enum class FeatureCode : uint16_t { @@ -47,6 +50,7 @@ namespace HIDPP { PointerSpeed = 0x2205, }; + // ----------------------------------------------------------------------------------------------- /// Hid++ 2.0 error codes enum class Error : uint8_t { NoError = 0, @@ -61,8 +65,7 @@ namespace HIDPP { Unsupported = 9, }; - const char* toString(Error e); - + // ----------------------------------------------------------------------------------------------- namespace Commands { constexpr uint8_t SetRegister = 0x80; constexpr uint8_t GetRegister = 0x81; @@ -70,6 +73,21 @@ namespace HIDPP { constexpr uint8_t GetLongRegister = 0x83; } + // ----------------------------------------------------------------------------------------------- + struct ProtocolVersion { + uint8_t major = 0; + uint8_t minor = 0; + + bool smallerThan(uint8_t otherMajor, uint8_t otherMinor) const { + return (major < otherMajor) ? true : (minor < otherMinor) ? true : false; + } + + bool operator<(const ProtocolVersion& other) const { + return smallerThan(other.major, other.minor); + } + }; + + // ----------------------------------------------------------------------------------------------- /// Hidpp message class, heavily inspired by this library: https://github.com/cvuchener/hidpp class Message final { @@ -90,10 +108,14 @@ namespace HIDPP { Message(Type type); /// Create a message with the given properties and payload. Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, uint8_t swId, - Data payload); + Data payload = {}); /// Create a message with the given properties and payload. /// An internal default is used as software id for the message. - Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, Data payload); + Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, + Data payload = {}); + + Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, Data payload = {}); + Message(Type type, uint8_t deviceIndex, Data payload = {}); /// Create a message from raw data. /// If the data is not a valid Hidpp message, this will result in an invalid HID++ message. @@ -101,6 +123,7 @@ namespace HIDPP { Message(Message&& msg) = default; Message(const Message& msg) = default; + Message& operator=(Message&&) = default; inline bool operator==(const Message& other) const { return m_data == other.m_data; } @@ -150,13 +173,17 @@ namespace HIDPP { bool isErrorResponseTo(const Message& other) const; auto data() { return m_data.data(); } + const auto data() const { return m_data.data(); } auto dataSize() { return m_data.size(); } - auto operator[](size_t i) { return m_data.operator[](i); } + auto& operator[](size_t i) { return m_data.operator[](i); } + const auto& operator[](size_t i) const { return m_data.operator[](i); } QString hex() const; private: Data m_data; }; + + Message::Data getRandomPingPayload(); } //end of HIDPP namespace // ------------------------------------------------------------------------------------------------- @@ -172,8 +199,6 @@ class HidppConnectionInterface HidppError, }; - static const char* toString(MsgResult); - using SendResultCallback = std::function; using RequestResultCallback = std::function; @@ -216,6 +241,34 @@ namespace HIDPP { constexpr uint8_t SHORT_WIRELESS_NOTIFICATION_CODE = 0x41; // TODO MOVE RENAME /// } + class FirmwareInfo + { + public: + enum class FirmwareType : uint8_t { + MainApp = 0, + Bootloader = 1, + Hardware = 2, + Other = 3, + Invalid = 0xff + }; + + FirmwareInfo() = default; + FirmwareInfo(Message&& msg); + FirmwareInfo(const FirmwareInfo&) = default; + FirmwareInfo(FirmwareInfo&&) = default; + FirmwareInfo& operator=(FirmwareInfo&&) = default; + + FirmwareType firmwareType() const; + QString firmwarePrefix() const; + uint16_t firmwareVersion() const; + uint16_t firmwareBuild() const; + bool isValid() const { return firmwareType() != FirmwareType::Invalid; } + + private: + HIDPP::Message m_rawMsg; + }; + + // ----------------------------------------------------------------------------------------------- /// Class to get and store set of supported features and additional information /// for a HID++ 2.0 device (although very much specialized for the Logitech Spotlight). class FeatureSet : public QObject, public async::Async @@ -227,36 +280,43 @@ namespace HIDPP { FeatureSet(HidppConnectionInterface* connection, QObject* parent = nullptr); - void initFromDevice(); + void initFromDevice(std::function cb); State state() const; - uint8_t getFeatureIndex(FeatureCode fc) const; - bool supportFeatureCode(FeatureCode fc) const; - auto getFeatureCount() const { return m_featureTable.size(); } - void populateFeatureTable(); + uint8_t featureIndex(FeatureCode fc) const; + bool featureCodeSupported(FeatureCode fc) const; + auto featureCount() const { return m_featureTable.size(); } + + //void populateFeatureTable(); signals: void stateChanged(State s); private: - struct ProtocolVersion { - uint8_t major = 0; - uint8_t minor = 0; - }; using MsgResult = HidppConnectionInterface::MsgResult; - void getFeatureIndex(FeatureCode fc, std::function cb); - - void getProtocolVersion(std::function cb); + using FeatureTable = std::map; - uint8_t getFeatureIndexFromDevice(FeatureCode fc); - uint8_t getFeatureCountFromDevice(uint8_t featureSetID); - QByteArray getFirmwareVersionFromDevice(); + void getFeatureIndex(FeatureCode fc, std::function cb); + void getFeatureCount(std::function cb); + void getFirmwareCount(std::function cb); + void getFeatureIds(uint8_t featureSetIndex, uint8_t count, + std::function cb); + void getMainFirmwareInfo(std::function cb); + void getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t current, + std::function cb); + void getFirmwareInfo(uint8_t fwIndex, uint8_t entity, + std::function cb); + + void setState(State s); HidppConnectionInterface* m_connection = nullptr; - std::map m_featureTable; - - ProtocolVersion m_protocolVersion; + FeatureTable m_featureTable; + FirmwareInfo m_mainFirmwareInfo; State m_state = State::Uninitialized; }; } //end namespace HIDPP + +const char* toString(HIDPP::Error e); +const char* toString(HidppConnectionInterface::MsgResult r); +const char* toString(HIDPP::FeatureSet::State s); diff --git a/src/iconwidgets.h b/src/iconwidgets.h index 133ee6d5..7e7330f6 100644 --- a/src/iconwidgets.h +++ b/src/iconwidgets.h @@ -7,7 +7,7 @@ #include // ------------------------------------------------------------------------------------------------- -/// @brief Icon button class used throughout the application's widget based dialogs. +/// Icon button class used throughout the application's widget based dialogs. class IconButton : public QToolButton { Q_OBJECT @@ -17,7 +17,7 @@ class IconButton : public QToolButton }; // ------------------------------------------------------------------------------------------------- -/// @brief Icon label class used throughout the application's widget based dialogs. +/// Icon label class used throughout the application's widget based dialogs. class IconLabel : public QLabel { Q_OBJECT diff --git a/src/inputmapconfig.h b/src/inputmapconfig.h index f138f0d6..ea2f96a8 100644 --- a/src/inputmapconfig.h +++ b/src/inputmapconfig.h @@ -13,7 +13,7 @@ class ActionTypeDelegate; class InputSeqDelegate; // ------------------------------------------------------------------------------------------------- -/// @brief TODO +/// Item for the input map model. struct InputMapModelItem { KeyEventSequence deviceSequence; std::shared_ptr action; @@ -21,7 +21,7 @@ struct InputMapModelItem { }; // ------------------------------------------------------------------------------------------------- -/// @brief TODO +/// Input map configuration table model. class InputMapConfigModel : public QAbstractTableModel { Q_OBJECT @@ -63,7 +63,7 @@ class InputMapConfigModel : public QAbstractTableModel }; // ------------------------------------------------------------------------------------------------- -/// @brief TODO +/// Input map configuration view. struct InputMapConfigView : public QTableView { Q_OBJECT diff --git a/src/spotlight.cc b/src/spotlight.cc index 1f12bee8..14cdbdd1 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -145,32 +145,30 @@ int Spotlight::connectDevices() if (dc->hasHidppSupport()) { auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc); - if (true - //addHidppInputHandler(hidppCon) - ) + if (hidppCon) { // connect to hidpp sub connection signals connect(&*hidppCon, &SubHidppConnection::receivedBatteryInfo, dc.get(), &DeviceConnection::setBatteryInfo); - auto hidppActivated = [this, dc]() { - if (std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), - dc->deviceId()) == m_activeDeviceIds.cend()) { - logInfo(device) << dc->deviceName() << "is now active."; - m_activeDeviceIds.emplace_back(dc->deviceId()); - emit deviceActivated(dc->deviceId(), dc->deviceName()); - } - }; - auto hidppDeactivated = [this, dc]() { - auto it = std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), dc->deviceId()); - if (it != m_activeDeviceIds.cend()) { - logInfo(device) << dc->deviceName() << "is deactivated."; - m_activeDeviceIds.erase(it); - emit deviceDeactivated(dc->deviceId(), dc->deviceName()); - } - }; - connect(&*hidppCon, &SubHidppConnection::activated, dc.get(), hidppActivated); - connect(&*hidppCon, &SubHidppConnection::deactivated, dc.get(), hidppDeactivated); - connect(&*hidppCon, &SubHidppConnection::destroyed, dc.get(), hidppDeactivated); + // auto hidppActivated = [this, dc]() { + // if (std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), + // dc->deviceId()) == m_activeDeviceIds.cend()) { + // logInfo(device) << dc->deviceName() << "is now active."; + // m_activeDeviceIds.emplace_back(dc->deviceId()); + // emit deviceActivated(dc->deviceId(), dc->deviceName()); + // } + // }; + // auto hidppDeactivated = [this, dc]() { + // auto it = std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), dc->deviceId()); + // if (it != m_activeDeviceIds.cend()) { + // logInfo(device) << dc->deviceName() << "is deactivated."; + // m_activeDeviceIds.erase(it); + // emit deviceDeactivated(dc->deviceId(), dc->deviceName()); + // } + // }; + // connect(&*hidppCon, &SubHidppConnection::activated, dc.get(), hidppActivated); + // connect(&*hidppCon, &SubHidppConnection::deactivated, dc.get(), hidppDeactivated); + // connect(&*hidppCon, &SubHidppConnection::destroyed, dc.get(), hidppDeactivated); return hidppCon; } From 33cdb12ec85e680f38a1eadbaa5912bbb8d7ac49 Mon Sep 17 00:00:00 2001 From: Jahn Date: Thu, 9 Sep 2021 01:49:52 +0200 Subject: [PATCH 064/110] Refactoring of hid++ functionality part 3. --- .../templates/projecteur-icons-def.h | 9 +- src/aboutdlg.cc | 3 +- src/aboutdlg.h | 3 +- src/actiondelegate.cc | 4 +- src/actiondelegate.h | 3 +- src/asynchronous.h | 112 ++++-- src/colorselector.cc | 4 +- src/colorselector.h | 3 +- src/device-hidpp.cc | 318 ++++++++++-------- src/device-hidpp.h | 10 +- src/device-vibration.cc | 10 +- src/device-vibration.h | 6 +- src/device.cc | 73 ++-- src/device.h | 51 +-- src/deviceinput.cc | 1 + src/deviceswidget.cc | 241 ++++++------- src/extra-devices.cc.in | 1 + src/hidpp.cc | 112 ++++-- src/hidpp.h | 45 ++- src/iconwidgets.cc | 4 +- src/iconwidgets.h | 3 +- src/imageitem.cc | 4 +- src/imageitem.h | 3 +- src/inputmapconfig.cc | 4 +- src/inputmapconfig.h | 3 +- src/inputseqedit.cc | 4 +- src/inputseqedit.h | 3 +- src/linuxdesktop.cc | 32 +- src/linuxdesktop.h | 5 +- src/logging.cc | 4 +- src/logging.h | 3 +- src/main.cc | 4 +- src/nativekeyseqedit.cc | 4 +- src/nativekeyseqedit.h | 3 +- src/preferencesdlg.cc | 4 +- src/preferencesdlg.h | 3 +- src/projecteur-icons-def.h | 3 +- src/projecteurapp.cc | 4 +- src/projecteurapp.h | 4 +- src/settings.cc | 4 +- src/settings.h | 3 +- src/spotlight.cc | 31 +- src/spotlight.h | 3 +- src/spotshapes.cc | 4 +- src/spotshapes.h | 3 +- src/virtualdevice.cc | 4 +- src/virtualdevice.h | 5 +- 47 files changed, 661 insertions(+), 506 deletions(-) diff --git a/icons/icon-font/templates/projecteur-icons-def.h b/icons/icon-font/templates/projecteur-icons-def.h index ed7986c1..abb2f78b 100644 --- a/icons/icon-font/templates/projecteur-icons-def.h +++ b/icons/icon-font/templates/projecteur-icons-def.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once // Auto generated defines for icon-font with `fontcustom` @@ -6,10 +7,10 @@ namespace Font { enum Icon { -<% @glyphs.each do |key, value| - name = key.to_s.delete_prefix("iconmonstr-") +<% @glyphs.each do |key, value| + name = key.to_s.delete_prefix("iconmonstr-") name = name.gsub(/^[0-9]|[^A-Za-z0-9]/, '_') %><%= " #{name} = 0x#{value[:codepoint].to_s(16)}, // #{value[:source]}" %> -<% end +<% end %> }; } diff --git a/src/aboutdlg.cc b/src/aboutdlg.cc index 64ae1654..52497d24 100644 --- a/src/aboutdlg.cc +++ b/src/aboutdlg.cc @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #include "aboutdlg.h" diff --git a/src/aboutdlg.h b/src/aboutdlg.h index 6ccd1a3c..9b295364 100644 --- a/src/aboutdlg.h +++ b/src/aboutdlg.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index 7d0e52b7..212251a8 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "actiondelegate.h" #include "inputmapconfig.h" diff --git a/src/actiondelegate.h b/src/actiondelegate.h index 5f759b21..60b8755b 100644 --- a/src/actiondelegate.h +++ b/src/actiondelegate.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/asynchronous.h b/src/asynchronous.h index 406844b6..cbf30912 100644 --- a/src/asynchronous.h +++ b/src/asynchronous.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include @@ -10,15 +11,55 @@ #endif #include +#include #include +#include namespace async { +// capture_call helper method and apply for C++14 taken from here: +// https://stackoverflow.com/a/49902823 + +// Implementation detail of a simplified std::apply from C++17 +template +constexpr decltype(auto) +apply_impl(F&& f, Tuple&& t, std::index_sequence){ + return static_cast(f)(std::get(static_cast(t)) ...); +} + +// Implementation of a simplified std::apply from C++17 +template +constexpr decltype(auto) apply(F&& f, Tuple&& t){ + return apply_impl( + static_cast(f), static_cast(t), + std::make_index_sequence>::value>{}); +} + +// Capture args and add them as additional arguments +template +auto capture_call(Lambda&& lambda, Args&& ... args){ + return [ + lambda = std::forward(lambda), + capture_args = std::make_tuple(std::forward(args) ...) + ](auto&& ... original_args)mutable{ + return async::apply([&lambda](auto&& ... args){ + lambda(std::forward(args) ...); + }, + std::tuple_cat( + std::forward_as_tuple(original_args ...), + async::apply([](auto&& ... args){ + return std::forward_as_tuple( + std::move(args) ...); + }, std::move(capture_args)) + )); + }; +} + #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) // Invoke a (lambda) function for context QObject with queued connection. template -void invoke(QObject* context, F function) { - QMetaObject::invokeMethod(context, std::move(function), Qt::QueuedConnection); +void invoke(QObject* context, F&& function) { + QMetaObject::invokeMethod(context, std::forward(function), Qt::QueuedConnection); } #else // ... older Qt versions < 5.10 @@ -38,7 +79,6 @@ void invoke(QObject* context, F&& function) { } #endif - // --- Helpers to deduce std::function type from a lambda. template struct remove_member; @@ -55,14 +95,12 @@ struct remove_member { /// Create a safe function object, guaranteed to be invoked in the context of /// the given QObject context. -template -std::function makeSafeCallback(QObject* context, - std::function f, - bool forceQueued = true) +template +auto makeSafeCallback_impl(QObject* context, F&& f, std::function, bool forceQueued) { QPointer ctxPtr(context); - std::function res = - [ctxPtr, forceQueued, func = std::move(f)](Args... args) { + return [ctxPtr, forceQueued, f=std::forward(f)](Args&&... args) mutable + { // Check if context object is still valid if (ctxPtr.isNull()) { return; @@ -70,23 +108,24 @@ std::function makeSafeCallback(QObject* context, #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QMetaObject::invokeMethod(ctxPtr, - std::bind(std::move(func), std::forward(args)...), + capture_call(std::forward(f), std::forward(args)...), forceQueued ? Qt::QueuedConnection : Qt::AutoConnection); // Note: if forceQueued is false and current thread is the same as // the context thread -> execute directly #else // For Qt < 5.10 the call is always queued via the event queue - async::invoke(ctxPtr, std::bind(std::move(func), std::forward(args)...)); + async::invoke(ctxPtr, capture_call(std::forward(f), std::forward(args)...)); #endif }; - return res; } +/// Create a safe function object, guaranteed to be invoked in the context of +/// the given QObject context. template -auto makeSafeCallback(QObject* context, F f, bool forceQueued = true) { - using ft = decltype(&F::operator()); - std::function::type> func = std::move(f); - return async::makeSafeCallback(context, std::move(func), forceQueued); +auto makeSafeCallback(QObject* context, F&& f, bool forceQueued = true) { + using sig = decltype(&F::operator()); + using ft = std::function::type>; + return async::makeSafeCallback_impl(context, std::forward(f), ft{}, forceQueued); } /// Deriving from this class will makeSafeCallback and postSelf methods for QObject based @@ -105,21 +144,21 @@ class Async protected: /// Returns a function object that is guaranteed to be invoked in the own thread context. template - auto makeSafeCallback(F f) { - return async::makeSafeCallback(static_cast(this), std::move(f)); + auto makeSafeCallback(F&& f) { + return async::makeSafeCallback(static_cast(this), std::forward(f)); } /// Post a function to the own event loop. template - void postSelf(F function) { - async::invoke(static_cast(this), std::move(function)); + void postSelf(F&& function) { + async::invoke(static_cast(this), std::forward(function)); } public: /// Post a task to the object's event loop. template - void postTask(Task task) { - postSelf(std::move(task)); + void postTask(Task&& task) { + postSelf(std::forward(task)); } template @@ -128,26 +167,29 @@ class Async /// Post a task with no return value and provide a callback. template typename std::enable_if_t> - postCallback(Task task, Callback callback) + postCallback(Task&& task, Callback&& callback) { - auto wrapper = [task = std::move(task), callback = std::move(callback)]() mutable - { - task(); - callback(); - }; - postSelf(std::move(wrapper)); + postSelf( + [task = std::forward(task), callback = std::forward(callback)]() mutable + { + task(); + callback(); + } + ); } /// Post a task with return value and a callback that takes the return value /// as an argument. template typename std::enable_if_t> - postCallback(Task task, Callback callback) + postCallback(Task&& task, Callback&& callback) { - auto wrapper = [task = std::move(task), callback = std::move(callback)]() mutable { - callback(task()); - }; - postSelf(std::move(wrapper)); + postSelf( + [task = std::forward(task), callback = std::forward(callback)]() mutable + { + callback(task()); + } + ); } }; diff --git a/src/colorselector.cc b/src/colorselector.cc index 8789ab62..5a0187d2 100644 --- a/src/colorselector.cc +++ b/src/colorselector.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "colorselector.h" #include diff --git a/src/colorselector.h b/src/colorselector.h index 39d61977..be922c87 100644 --- a/src/colorselector.h +++ b/src/colorselector.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index e593cd5f..8e957b56 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -264,7 +264,7 @@ void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatc sendRequest(std::move(queueItem.message), makeSafeCallback( [this, batch = std::move(batch), results = std::move(results), coe, batchCb = std::move(batchCb), resultCb = std::move(queueItem.callback)] - (MsgResult result, HIDPP::Message replyMessage) mutable + (MsgResult result, HIDPP::Message&& replyMessage) mutable { // Add result to results vector results.push_back(result); @@ -304,10 +304,17 @@ std::shared_ptr SubHidppConnection::create(const DeviceScan: } // ------------------------------------------------------------------------------------------------- -void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) { - if (!hasFlags(DeviceFlags::Vibrate)) return; +void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length, + RequestResultCallback cb) +{ + const uint8_t pcIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PresenterControl); + + if (pcIndex == 0) + { + if (cb) cb(MsgResult::FeatureNotSupported, HIDPP::Message()); + return; + } - // TODO put in HIDPP // TODO generalize features and protocol for proprietary device features like vibration // for not only the Spotlight device. // @@ -317,25 +324,20 @@ void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length) { // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; length = length > 10 ? 10 : length; // length should be between 0 to 10. - const uint8_t pcIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PresenterControl); + using namespace HIDPP; - // const uint8_t vibrateCmd[] = {HIDPP::Bytes::SHORT_MSG, - // HIDPP::Bytes::MSG_TO_SPOTLIGHT, - // pcIndex, - // m_featureSet.getRandomFunctionCode(0x10), - // length, - // 0xe8, - // intensity}; + Message vibrateMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, pcIndex, 1, { length, 0xe8, intensity }); - if (pcIndex) sendData(std::move(vibrateMsg)); + + sendRequest(std::move(vibrateMsg), std::move(cb)); } // ------------------------------------------------------------------------------------------------- void SubHidppConnection::queryBatteryStatus() { - // TODO put parts in HIDPP + // TODO refactor battery status handling // if (hasFlags(DeviceFlag::ReportBattery)) { // const uint8_t batteryFeatureIndex = // m_featureSet.getFeatureIndex(HIDPP::FeatureCode::BatteryStatus); @@ -353,23 +355,25 @@ void SubHidppConnection::queryBatteryStatus() } // ------------------------------------------------------------------------------------------------- -void SubHidppConnection::setPointerSpeed(uint8_t -//level -) +void SubHidppConnection::setPointerSpeed(uint8_t speed, + std::function cb) { const uint8_t psIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PointerSpeed); - if (psIndex == 0x00) return; + if (psIndex == 0x00) + { + if (cb) cb(MsgResult::FeatureNotSupported, HIDPP::Message()); + return; + } + + speed = (speed > 0x09) ? 0x09 : speed; // speed should be in range of 0-9 + // Pointer speed sent to the device with values 0x10 - 0x19 + const uint8_t pointerSpeed = 0x10 & speed; - // level = (level > 0x09) ? 0x09 : level; // level should be in range of 0-9 - // uint8_t pointerSpeed = 0x10 & level; // pointer speed sent to device are between 0x10 - 0x19 (hence ten speed levels) - // const uint8_t pointerSpeedCmd[] = {HIDPP::Bytes::SHORT_MSG, - // HIDPP::Bytes::MSG_TO_SPOTLIGHT, - // psIndex, - // m_featureSet.getRandomFunctionCode(0x10), - // pointerSpeed, - // 0x00, - // 0x00}; - // sendData(pointerSpeedCmd, sizeof(pointerSpeedCmd)); + sendRequest( + HIDPP::Message(HIDPP::Message::Type::Long, HIDPP::DeviceIndex::WirelessDevice1, + psIndex, 1, HIDPP::Message::Data{pointerSpeed}), + std::move(cb) + ); } // ------------------------------------------------------------------------------------------------- @@ -473,7 +477,7 @@ void SubHidppConnection::initReceiver(std::function cb) }}; sendRequestBatch(std::move(batch), - makeSafeCallback([this, cb=std::move(cb)](std::vector results) + makeSafeCallback([this, cb=std::move(cb)](std::vector&& results) { setReceiverState(results.back() == MsgResult::Ok ? ReceiverState::Initialized : ReceiverState::Error); @@ -514,9 +518,24 @@ void SubHidppConnection::initPresenter(std::function cb) setPresenterState(PresenterState::Error); break; } - case FState::Initialized: { - setPresenterState(PresenterState::Initialized_Online); - break; + case FState::Initialized: + { + logDebug(hid) << tr("Received %1 supported features from device. (%2)") + .arg(m_featureSet.featureCount()).arg(path()); + + updateDeviceFlags(); + initFeatures(makeSafeCallback( + [this, cb=std::move(cb)](std::map&& resultMap) + { + if (resultMap.size()) { + for (const auto& res : resultMap) { + logDebug(hid) << tr("InitFeature result %1 => %2").arg(toString(res.first)).arg(toString(res.second)); + } + } + setPresenterState(PresenterState::Initialized_Online); + if (cb) cb(m_presenterState); + })); + return; } } if (cb) cb(m_presenterState); @@ -524,6 +543,122 @@ void SubHidppConnection::initPresenter(std::function cb) }); } +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::initFeatures( + std::function&&)> cb) +{ + using namespace HIDPP; + using ResultMap = std::map; + + RequestBatch batch; + auto resultMap = std::make_shared(); + + // TODO: Is resetting the device necessary? + // Reset spotlight device, if supported + if (const auto resetFeatureIndex = m_featureSet.featureIndex(FeatureCode::Reset)) + { + batch.emplace(RequestBatchItem { + Message(Message::Type::Long, DeviceIndex::WirelessDevice1, resetFeatureIndex, 1), + [resultMap](MsgResult res, Message&&) { + resultMap->emplace(FeatureCode::Reset, res); + } + }); + } + + // Enable Next and back button on hold functionality. + if (const auto contrFeatureIndex = m_featureSet.featureIndex(FeatureCode::ReprogramControlsV4)) + { + if (hasFlags(DeviceFlags::NextHold)) + { + batch.emplace(RequestBatchItem { + Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3, + Message::Data{0x00, 0xda, 0x33}), + [resultMap](MsgResult res, Message&&) { + resultMap->emplace(FeatureCode::ReprogramControlsV4, res); + } + }); + } + + if (hasFlags(DeviceFlags::BackHold)) + { + batch.emplace(RequestBatchItem { + Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3, + Message::Data{0x00, 0xdc, 0x33}), + [resultMap](MsgResult res, Message&&) { + resultMap->emplace(FeatureCode::ReprogramControlsV4, res); + } + }); + } + } + + if (const auto psFeatureIndex = m_featureSet.featureIndex(FeatureCode::PointerSpeed)) + { + // Reset pointer speed to 0x14 - the device accepts values from 0x10 to 0x19 + batch.emplace(RequestBatchItem { + HIDPP::Message(HIDPP::Message::Type::Long, HIDPP::DeviceIndex::WirelessDevice1, + psFeatureIndex, 1, HIDPP::Message::Data{0x14}), + [resultMap](MsgResult res, Message&&) { + resultMap->emplace(FeatureCode::PointerSpeed, res); + } + }); + } + + sendRequestBatch(std::move(batch), + [resultMap=std::move(resultMap), cb=std::move(cb)](std::vector&&) { + if (cb) cb(std::move(*resultMap)); + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::updateDeviceFlags() +{ + DeviceFlags featureFlagsSet = DeviceFlag::NoFlags; + DeviceFlags featureFlagsUnset = DeviceFlag::NoFlags; + + if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::PresenterControl)) { + featureFlagsSet |= DeviceFlag::Vibrate; + logDebug(hid) << tr("Subdevice '%1' reported %2 support.") + .arg(path()).arg(toString(HIDPP::FeatureCode::PresenterControl)); + } else { + featureFlagsUnset |= DeviceFlag::Vibrate; + } + + if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::BatteryStatus)) { + featureFlagsSet |= DeviceFlag::ReportBattery; + logDebug(hid) << tr("Subdevice '%1' reported %2 support.") + .arg(path()).arg(toString(HIDPP::FeatureCode::BatteryStatus)); + } else { + featureFlagsUnset |= DeviceFlag::ReportBattery; + } + + if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::ReprogramControlsV4)) { + auto& reservedInputs = m_inputMapper->getReservedInputs(); + reservedInputs.clear(); + featureFlagsSet |= DeviceFlags::NextHold; + featureFlagsSet |= DeviceFlags::BackHold; + reservedInputs.emplace_back(ReservedKeyEventSequence::NextHoldInfo); + reservedInputs.emplace_back(ReservedKeyEventSequence::BackHoldInfo); + logDebug(hid) << tr("Subdevice '%1' reported %2 support.") + .arg(path()).arg(toString(HIDPP::FeatureCode::ReprogramControlsV4)); + } + else { + featureFlagsUnset |= DeviceFlags::NextHold; + featureFlagsUnset |= DeviceFlags::BackHold; + } + + if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::PointerSpeed)) { + featureFlagsSet |= DeviceFlags::PointerSpeed; + logDebug(hid) << tr("Subdevice '%1' reported %2 support.") + .arg(path()).arg(toString(HIDPP::FeatureCode::PointerSpeed)); + } + else { + featureFlagsUnset |= DeviceFlags::BackHold; + } + + setFlags(featureFlagsUnset, false); + setFlags(featureFlagsSet, true); +} + // ------------------------------------------------------------------------------------------------- void SubHidppConnection::subDeviceInit() { @@ -539,100 +674,6 @@ void SubHidppConnection::subDeviceInit() logDebug(hid) << tr("subDeviceInit, checkAndUpdatePresenterState = %1").arg(toString(ps)); })); })); - - // DeviceFlags featureFlags = DeviceFlag::NoFlags; - // Read HID++ FeatureSet (Feature ID and Feature Code pairs) from logitech device - // setNotifiersEnabled(false); - // setReadNotifierEnabled(false); // TODO remove ... implement populatefeaturetable via async.. - // if (m_featureSet.getFeatureCount() == 0) m_featureSet.populateFeatureTable(); - // if (m_featureSet.getFeatureCount()) { - // logDebug(hid) << "Loaded" << m_featureSet.getFeatureCount() << "features for" << path(); - // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::PresenterControl)) { - // featureFlags |= DeviceFlag::Vibrate; - // logDebug(hid) << "SubDevice" << path() << "reported Vibration capabilities."; - // } - // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::BatteryStatus)) { - // featureFlags |= DeviceFlag::ReportBattery; - // logDebug(hid) << "SubDevice" << path() << "can communicate battery information."; - // } - // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::ReprogramControlsV4)) { - // auto& reservedInputs = m_inputMapper->getReservedInputs(); - // reservedInputs.clear(); - // featureFlags |= DeviceFlags::NextHold; - // featureFlags |= DeviceFlags::BackHold; - // reservedInputs.emplace_back(ReservedKeyEventSequence::NextHoldInfo); - // reservedInputs.emplace_back(ReservedKeyEventSequence::BackHoldInfo); - // logDebug(hid) << "SubDevice" << path() << "can send next and back hold event."; - // } - // if (m_featureSet.supportFeatureCode(HIDPP::FeatureCode::PointerSpeed)) { - // featureFlags |= DeviceFlags::PointerSpeed; - // } - // } - // else { - // logWarn(hid) << "Loading FeatureSet for" << path() << "failed."; - // logInfo(hid) << "Device might be inactive. Press any button on device to activate it."; - // } - // setFlags(featureFlags, true); - // setReadNotifierEnabled(true); - - // TODO: implement - // // Reset spotlight device - // if (m_featureSet.getFeatureCount()) { - // const auto resetIndex = m_featureSet.getFeatureIndex(FeatureCode::Reset); - // if (resetIndex) { - // QTimer::singleShot(delay_ms*msgCount, this, [this, resetIndex](){ - // const uint8_t data[] = {HIDPP::Bytes::SHORT_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, - // resetIndex, m_featureSet.getRandomFunctionCode(0x10), 0x00, 0x00, 0x00}; sendData(data, - // sizeof(data));}); - // msgCount++; - // } - // } - // Device Resetting complete ------------------------------------------------- - - // TODO: implement - // if (m_busType == BusType::Usb) { - // // Ping spotlight device for checking if is online - // // the response will have the version for HID++ protocol. - // QTimer::singleShot(delay_ms*msgCount, this, [this](){pingSubDevice();}); - // msgCount++; - // } else if (m_busType == BusType::Bluetooth) { - // // Bluetooth connection do not respond to ping. - // // Hence, we are faking a ping response here. - // // Bluetooth connection mean HID++ v2.0+. - // // Setting version to 6.4: same as USB connection. - // setHIDppProtocol(6.4); - // } - - // TODO implement - // Enable Next and back button on hold functionality. - const auto rcIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::ReprogramControlsV4); - if (rcIndex) { - // if (hasFlags(DeviceFlags::NextHold)) { - // QTimer::singleShot(delay_ms*msgCount, this, [this, rcIndex](){ - // const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, - // rcIndex, m_featureSet.getRandomFunctionCode(0x30), 0x00, 0xda, 0x33, - // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 0x00, 0x00, 0x00}; - // sendData(data, sizeof(data));}); - // msgCount++; - // } - - // if (hasFlags(DeviceFlags::BackHold)) { - // QTimer::singleShot(delay_ms*7777777msgCount, this, [this, rcIndex](){ - // const uint8_t data[] = {HIDPP::Bytes::LONG_MSG, HIDPP::Bytes::MSG_TO_SPOTLIGHT, - // rcIndex, m_featureSet.getRandomFunctionCode(0x30), 0x00, 0xdc, 0x33, - // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // 0x00, 0x00, 0x00}; - // sendData(data, sizeof(data));}); - // msgCount++; - // } - } - - // Reset pointer speed to default level of 0x04 (5th level) - if (hasFlags(DeviceFlags::PointerSpeed)) setPointerSpeed(0x04); - - - // m_featureSet.initFromDevice(); } // ------------------------------------------------------------------------------------------------- @@ -764,10 +805,14 @@ void SubHidppConnection::onHidppDataAvailable(int fd) return; } - if (!msg.isValid()) { - // TODO check for move pointer messages on hid++ device, else log invalid message - logDebug(hid) << tr("Received invalid HID++ message " - "'%1' from %2").arg(msg.hex(), path()); + if (!msg.isValid()) + { + if (msg[0] == 0x02) { + // just ignore regular HID reports from the Logitech Spotlight + } + else { + logDebug(hid) << tr("Received invalid HID++ message '%1' from %2").arg(msg.hex(), path()); + } return; } @@ -810,10 +855,19 @@ void SubHidppConnection::onHidppDataAvailable(int fd) } m_requests.erase(it); } - else { + else if (msg.softwareId() == 0 || msg.subId() < 0x80) + { + // Event/Notification + // TODO Notify listeners registered to the notification type + // check for wireless notification code 0x41 from usb dongle (see hid++ 1.0 docs) + + logDebug(hid) << tr("Received notification (%1) on %2").arg(msg.hex()).arg(path()); + } + else + { // TODO check for device event messages, that don't require a request logWarn(hid) << tr("Received hidpp message " - "'%1' without matching request.").arg(qPrintable(msg.hex())); + "'%1' without matching request.").arg(msg.hex()); } } diff --git a/src/device-hidpp.h b/src/device-hidpp.h index 8e3aecd0..0ac62c51 100644 --- a/src/device-hidpp.h +++ b/src/device-hidpp.h @@ -60,9 +60,10 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void sendPing(RequestResultCallback cb); HIDPP::ProtocolVersion protocolVersion() const; - void queryBatteryStatus() override; - void sendVibrateCommand(uint8_t intensity, uint8_t length) override; - void setPointerSpeed(uint8_t level); + void queryBatteryStatus(); // TODO refactory battery info handling + void sendVibrateCommand(uint8_t intensity, uint8_t length, RequestResultCallback cb); + /// Set device pointer speed - speed needs to be in the range [0-9] + void setPointerSpeed(uint8_t speed, RequestResultCallback cb); signals: void receiverStateChanged(ReceiverState); @@ -74,6 +75,9 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void subDeviceInit(); void initReceiver(std::function); void initPresenter(std::function); + void updateDeviceFlags(); + /// Initializes features. Returns a map of initalized features and the result from it. + void initFeatures(std::function&&)> cb); void setReceiverState(ReceiverState rs); void setPresenterState(PresenterState ps); diff --git a/src/device-vibration.cc b/src/device-vibration.cc index 97c3aaf3..c87e3a3c 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -1,7 +1,9 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "device-vibration.h" -#include "device.h" +#include "device-hidpp.h" #include "hidpp.h" #include "iconwidgets.h" #include "logging.h" @@ -405,7 +407,7 @@ void VibrationSettingsWidget::setIntensity(uint8_t intensity) // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc) { - m_subDeviceConnection = sdc; + m_subDeviceConnection = qobject_cast(sdc); } // ------------------------------------------------------------------------------------------------- @@ -417,5 +419,5 @@ void VibrationSettingsWidget::sendVibrateCommand() const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); - m_subDeviceConnection->sendVibrateCommand(vint, vlen); + m_subDeviceConnection->sendVibrateCommand(vint, vlen, {}); } diff --git a/src/device-vibration.h b/src/device-vibration.h index 99d9bb6f..bd753491 100644 --- a/src/device-vibration.h +++ b/src/device-vibration.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include @@ -7,6 +8,7 @@ class QSpinBox; class SubDeviceConnection; +class SubHidppConnection; // ------------------------------------------------------------------------------------------------- class TimerWidget : public QWidget @@ -93,7 +95,7 @@ class VibrationSettingsWidget : public QWidget void lengthChanged(uint8_t length); private: - QPointer m_subDeviceConnection; + QPointer m_subDeviceConnection; QSpinBox* m_sbLength = nullptr; QSpinBox* m_sbIntensity = nullptr; }; diff --git a/src/device.cc b/src/device.cc index 05391d94..bd95d0fc 100644 --- a/src/device.cc +++ b/src/device.cc @@ -78,43 +78,43 @@ bool DeviceConnection::removeSubDevice(const QString& path) // ------------------------------------------------------------------------------------------------- -bool DeviceConnection::hasHidppSupport() const{ +bool DeviceConnection::hasHidppSupport() const { // HID++ only for Logitech devices return m_deviceId.vendorId == 0x046d; } -// ------------------------------------------------------------------------------------------------- -void DeviceConnection::queryBatteryStatus() -{ - for (const auto& sd: subDevices()) - { - if (sd.second->type() == ConnectionType::Hidraw - && sd.second->mode() == ConnectionMode::ReadWrite) - { - if (sd.second->hasFlags(DeviceFlag::ReportBattery)) sd.second->queryBatteryStatus(); - } - } -} - -// ------------------------------------------------------------------------------------------------- -void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) -{ - // TODO Remove / refactor with hid++ update 2 - const bool hasBattery = - std::any_of(m_subDeviceConnections.cbegin(), m_subDeviceConnections.cend(), [](const auto& sd) { - return sd.second->hasFlags(DeviceFlag::ReportBattery); - }); - - if (hasBattery && batteryData.length() == 3) - { - // Battery percent is only meaningful when battery is discharging. However, save them anyway. - m_batteryInfo.currentLevel - = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0) : 100); - m_batteryInfo.nextReportedLevel - = static_cast(batteryData.at(1) <= 100 ? batteryData.at(1): 100); - m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x07) ? batteryData.at(2): 0x07); - } -} +// // ------------------------------------------------------------------------------------------------- +// void DeviceConnection::queryBatteryStatus() +// { +// for (const auto& sd: subDevices()) +// { +// if (sd.second->type() == ConnectionType::Hidraw +// && sd.second->mode() == ConnectionMode::ReadWrite) +// { +// if (sd.second->hasFlags(DeviceFlag::ReportBattery)) sd.second->queryBatteryStatus(); +// } +// } +// } + +// // ------------------------------------------------------------------------------------------------- +// void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) +// { +// // TODO Refactor battery handling +// const bool hasBattery = +// std::any_of(m_subDeviceConnections.cbegin(), m_subDeviceConnections.cend(), [](const auto& sd) { +// return sd.second->hasFlags(DeviceFlag::ReportBattery); +// }); + +// if (hasBattery && batteryData.length() == 3) +// { +// // Battery percent is only meaningful when battery is discharging. However, save them anyway. +// m_batteryInfo.currentLevel +// = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0) : 100); +// m_batteryInfo.nextReportedLevel +// = static_cast(batteryData.at(1) <= 100 ? batteryData.at(1): 100); +// m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x07) ? batteryData.at(2): 0x07); +// } +// } // ------------------------------------------------------------------------------------------------- SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, @@ -174,12 +174,6 @@ QSocketNotifier* SubDeviceConnection::socketReadNotifier() { return m_readNotifier.get(); } -// ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::sendVibrateCommand(uint8_t, uint8_t) {} - -// ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::queryBatteryStatus() {} - // ------------------------------------------------------------------------------------------------- SubEventConnection::SubEventConnection(Token, const DeviceScan::SubDevice& sd) : SubDeviceConnection(sd, ConnectionType::Event, ConnectionMode::ReadOnly) {} @@ -376,7 +370,6 @@ ssize_t SubHidrawConnection::sendData(const void* msg, size_t msgLen) if (mode() != ConnectionMode::ReadWrite || !m_writeNotifier) { return errorResult; } // TODO check against m_writeNotifier? const auto res = ::write(m_writeNotifier->socket(), msg, msgLen); - logWarn(hid) << tr("Writing to '%1' len=%2 msg=%3").arg(path()).arg(msgLen).arg(qPrintable(QByteArray::fromRawData(static_cast(msg), msgLen).toHex())); if (static_cast(res) == msgLen) { logDebug(hid) << res << "bytes written to" << path() << "(" diff --git a/src/device.h b/src/device.h index 8a1c81e1..48eecb84 100644 --- a/src/device.h +++ b/src/device.h @@ -21,34 +21,6 @@ class QSocketNotifier; class SubDeviceConnection; class VirtualDevice; -// namespace HIDPP { -// class FeatureSet; -// } - -// namespace DeviceScan { -// struct SubDevice; -// } - -// ------------------------------------------------------------------------------------------------- -// Battery Status as returned on HID++ BatteryStatus feature code (0x1000) -enum class BatteryStatus : uint8_t {Discharging = 0x00, - Charging = 0x01, - AlmostFull = 0x02, - Full = 0x03, - SlowCharging = 0x04, - InvalidBattery = 0x05, - ThermalError = 0x06, - ChargingError = 0x07 - }; - -struct BatteryInfo -{ - uint8_t currentLevel = 0; - uint8_t nextReportedLevel = 0; - BatteryStatus status = BatteryStatus::Discharging; -}; - - // ------------------------------------------------------------------------------------------------- /// The main device connection class, which usually consists of one or multiple sub devices. class DeviceConnection : public QObject @@ -70,12 +42,12 @@ class DeviceConnection : public QObject bool removeSubDevice(const QString& path); const auto& subDevices() { return m_subDeviceConnections; } - // TODO ... battery status on device or subdevice level? - void queryBatteryStatus(); - auto getBatteryInfo(){ return m_batteryInfo; } + // // TODO ... battery status on device or subdevice level? + // void queryBatteryStatus(); + // auto getBatteryInfo(){ return m_batteryInfo; } -public slots: - void setBatteryInfo(const QByteArray& batteryData); +// public slots: +// void setBatteryInfo(const QByteArray& batteryData); signals: void subDeviceConnected(const DeviceId& id, const QString& path); @@ -91,7 +63,7 @@ public slots: std::shared_ptr m_inputMapper; ConnectionMap m_subDeviceConnections; - BatteryInfo m_batteryInfo; // TODO.. + // BatteryInfo m_batteryInfo; // TODO.. }; // ------------------------------------------------------------------------------------------------- @@ -164,11 +136,11 @@ class SubDeviceConnection : public QObject, public async::Async& inputMapper() const; QSocketNotifier* socketReadNotifier(); // Read notifier for Hidraw and Event connections for receiving data from device - // HID++ specific functions: These commands write on device and expect some return message - virtual bool isOnline() const { return false; }; - virtual void sendVibrateCommand(uint8_t intensity, uint8_t length); - virtual void queryBatteryStatus(); - virtual float getHIDppProtocol() const { return -1; }; + // // HID++ specific functions: These commands write on device and expect some return message + // // virtual bool isOnline() const { return false; }; + // // virtual void sendVibrateCommand(uint8_t intensity, uint8_t length); + // virtual void queryBatteryStatus(); + // virtual float getHIDppProtocol() const { return -1; }; signals: void flagsChanged(DeviceFlags f); @@ -238,4 +210,3 @@ class SubHidrawConnection : public SubDeviceConnection, public HidrawConnectionI private: void onHidrawDataAvailable(int fd); }; - diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 83106847..810c62f9 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -1,5 +1,6 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md + #include "deviceinput.h" #include "logging.h" diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 05c5a4bc..6620d118 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -124,127 +124,128 @@ QWidget* DevicesWidget::createDevicesWidget(Settings* settings, Spotlight* spotl } // ------------------------------------------------------------------------------------------------- -void DevicesWidget::updateDeviceDetails(Spotlight* spotlight) +void DevicesWidget::updateDeviceDetails(Spotlight* /* spotlight */) { - auto updateBatteryInfo = [this, spotlight]() { - auto curDeviceId = currentDeviceId(); - if (curDeviceId == invalidDeviceId) - return; - auto dc = spotlight->deviceConnection(curDeviceId); - dc->queryBatteryStatus(); - }; - - auto getDeviceDetails = [this, spotlight]() { - QString deviceDetails; - auto curDeviceId = currentDeviceId(); - if (curDeviceId == invalidDeviceId) - return tr("No Device Connected"); - auto dc = spotlight->deviceConnection(curDeviceId); - - const auto busTypeToString = [](BusType type) -> QString { - if (type == BusType::Usb) return "USB"; - if (type == BusType::Bluetooth) return "Bluetooth"; - return "Unknown"; - }; - - const QStringList subDeviceList = [dc](){ - QStringList subDeviceList; - auto accessText = [](ConnectionMode m){ - if (m == ConnectionMode::ReadOnly) return "ReadOnly"; - if (m == ConnectionMode::WriteOnly) return "WriteOnly"; - if (m == ConnectionMode::ReadWrite) return "ReadWrite"; - return "Unknown Access"; - }; - for (const auto& sd: dc->subDevices()) { - if (sd.second->path().size()) { - auto sds = sd.second; - subDeviceList.push_back(tr("%1%2[%3, %4, %5]").arg( - sds->path(), - (sds->path().length()<18)?"\t\t":"\t", - accessText(sds->mode()), - sds->isGrabbed()?"Grabbed":"", - sds->hasFlags(DeviceFlags::Hidpp)?"HID++":"" - )); - } - } - return subDeviceList; - }(); - auto batteryStatusText = [](BatteryStatus d){ - if (d == BatteryStatus::Discharging) return "Discharging"; - if (d == BatteryStatus::Charging) return "Charging"; - if (d == BatteryStatus::AlmostFull) return "Almost Full"; - if (d == BatteryStatus::Full) return "Full Charge"; - if (d == BatteryStatus::SlowCharging) return "Slow Charging"; - if (d == BatteryStatus::InvalidBattery || d == BatteryStatus::ThermalError || d == BatteryStatus::ChargingError) { - return "Charging Error"; - }; - return ""; - }; - - auto sDevices = dc->subDevices(); - bool isOnline = false, hasBattery = false, hasHIDPP = false; - float HIDPPversion = -1; - QStringList HIDPPfeatureText; - auto HIDppSubDevice = std::find_if(sDevices.cbegin(), sDevices.cend(), [](const auto& sd){ - return (sd.second->type() == ConnectionType::Hidraw && - sd.second->mode() == ConnectionMode::ReadWrite && - sd.second->hasFlags(DeviceFlag::Hidpp)); - }); - - if (HIDppSubDevice != sDevices.cend()) - { - auto dev = HIDppSubDevice->second; - hasHIDPP = true; - isOnline = dev->isOnline(); - hasBattery = dev->hasFlags(DeviceFlags::ReportBattery); - HIDPPversion = dev->getHIDppProtocol(); - // report HID++ features recognised by program (like vibration and others) - HIDPPfeatureText = [dev](){ - QStringList flagList; - if (dev->hasFlags(DeviceFlag::Vibrate)) flagList.push_back("Vibration"); - if (dev->hasFlags(DeviceFlag::ReportBattery)) flagList.push_back("Reports Battery"); - if (dev->hasFlags(DeviceFlag::NextHold)) flagList.push_back("Next Hold Button (Reprogrammed)"); - if (dev->hasFlags(DeviceFlag::BackHold)) flagList.push_back("Back Hold Button (Reprogrammed)"); - if (dev->hasFlags(DeviceFlag::PointerSpeed)) flagList.push_back("Variable Pointer Speed"); - return flagList; - }(); - } - - auto batteryInfoText = [dc, batteryStatusText](){ - auto batteryInfo= dc->getBatteryInfo(); - // Only show battery percent while discharging. - // Other cases, device do not report battery percentage correctly. - if (batteryInfo.status == BatteryStatus::Discharging) { - return tr("%1\% - %2% (%3)").arg( - QString::number(batteryInfo.currentLevel), - QString::number(batteryInfo.nextReportedLevel), - batteryStatusText(batteryInfo.status)); - } else { - return tr("%3").arg(batteryStatusText(batteryInfo.status)); - } - }; - - deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); - deviceDetails += tr("VendorId:\t%1\n").arg(hexId(dc->deviceId().vendorId)); - deviceDetails += tr("ProductId:\t%1\n").arg(hexId(dc->deviceId().productId)); - deviceDetails += tr("Phys:\t\t%1\n").arg(dc->deviceId().phys); - deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); - deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); - if (hasBattery && isOnline) deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); - if (hasHIDPP && !isOnline) deviceDetails += tr("\n\n\t Device not active. Press any key on device to update.\n"); - if (hasHIDPP && isOnline){ - deviceDetails += "\n"; - deviceDetails += tr("HID++ Version:\t%1\n").arg(HIDPPversion); - deviceDetails += tr("HID++ Features:\t%1\n").arg(HIDPPfeatureText.join(",\n\t\t")); - } - - return deviceDetails; - }; - - QTimer::singleShot(200, this, [updateBatteryInfo](){updateBatteryInfo();}); - if (m_deviceDetailsTextEdit) { - QTimer::singleShot(1000, this, [this, getDeviceDetails](){m_deviceDetailsTextEdit->setText(getDeviceDetails());}); - } + // // TODO refactor device details together with battery info handling + // auto updateBatteryInfo = [this, spotlight]() { + // auto curDeviceId = currentDeviceId(); + // if (curDeviceId == invalidDeviceId) + // return; + // auto dc = spotlight->deviceConnection(curDeviceId); + // dc->queryBatteryStatus(); + // }; + + // auto getDeviceDetails = [this, spotlight]() { + // QString deviceDetails; + // auto curDeviceId = currentDeviceId(); + // if (curDeviceId == invalidDeviceId) + // return tr("No Device Connected"); + // auto dc = spotlight->deviceConnection(curDeviceId); + + // const auto busTypeToString = [](BusType type) -> QString { + // if (type == BusType::Usb) return "USB"; + // if (type == BusType::Bluetooth) return "Bluetooth"; + // return "Unknown"; + // }; + + // const QStringList subDeviceList = [dc](){ + // QStringList subDeviceList; + // auto accessText = [](ConnectionMode m){ + // if (m == ConnectionMode::ReadOnly) return "ReadOnly"; + // if (m == ConnectionMode::WriteOnly) return "WriteOnly"; + // if (m == ConnectionMode::ReadWrite) return "ReadWrite"; + // return "Unknown Access"; + // }; + // for (const auto& sd: dc->subDevices()) { + // if (sd.second->path().size()) { + // auto sds = sd.second; + // subDeviceList.push_back(tr("%1%2[%3, %4, %5]").arg( + // sds->path(), + // (sds->path().length()<18)?"\t\t":"\t", + // accessText(sds->mode()), + // sds->isGrabbed()?"Grabbed":"", + // sds->hasFlags(DeviceFlags::Hidpp)?"HID++":"" + // )); + // } + // } + // return subDeviceList; + // }(); + // auto batteryStatusText = [](BatteryStatus d){ + // if (d == BatteryStatus::Discharging) return "Discharging"; + // if (d == BatteryStatus::Charging) return "Charging"; + // if (d == BatteryStatus::AlmostFull) return "Almost Full"; + // if (d == BatteryStatus::Full) return "Full Charge"; + // if (d == BatteryStatus::SlowCharging) return "Slow Charging"; + // if (d == BatteryStatus::InvalidBattery || d == BatteryStatus::ThermalError || d == BatteryStatus::ChargingError) { + // return "Charging Error"; + // }; + // return ""; + // }; + + // auto sDevices = dc->subDevices(); + // bool isOnline = false, hasBattery = false, hasHIDPP = false; + // float HIDPPversion = -1; + // QStringList HIDPPfeatureText; + // auto HIDppSubDevice = std::find_if(sDevices.cbegin(), sDevices.cend(), [](const auto& sd){ + // return (sd.second->type() == ConnectionType::Hidraw && + // sd.second->mode() == ConnectionMode::ReadWrite && + // sd.second->hasFlags(DeviceFlag::Hidpp)); + // }); + + // if (HIDppSubDevice != sDevices.cend()) + // { + // auto dev = HIDppSubDevice->second; + // hasHIDPP = true; + // isOnline = dev->isOnline(); + // hasBattery = dev->hasFlags(DeviceFlags::ReportBattery); + // HIDPPversion = dev->getHIDppProtocol(); + // // report HID++ features recognised by program (like vibration and others) + // HIDPPfeatureText = [dev](){ + // QStringList flagList; + // if (dev->hasFlags(DeviceFlag::Vibrate)) flagList.push_back("Vibration"); + // if (dev->hasFlags(DeviceFlag::ReportBattery)) flagList.push_back("Reports Battery"); + // if (dev->hasFlags(DeviceFlag::NextHold)) flagList.push_back("Next Hold Button (Reprogrammed)"); + // if (dev->hasFlags(DeviceFlag::BackHold)) flagList.push_back("Back Hold Button (Reprogrammed)"); + // if (dev->hasFlags(DeviceFlag::PointerSpeed)) flagList.push_back("Variable Pointer Speed"); + // return flagList; + // }(); + // } + + // auto batteryInfoText = [dc, batteryStatusText](){ + // auto batteryInfo= dc->getBatteryInfo(); + // // Only show battery percent while discharging. + // // Other cases, device do not report battery percentage correctly. + // if (batteryInfo.status == BatteryStatus::Discharging) { + // return tr("%1\% - %2% (%3)").arg( + // QString::number(batteryInfo.currentLevel), + // QString::number(batteryInfo.nextReportedLevel), + // batteryStatusText(batteryInfo.status)); + // } else { + // return tr("%3").arg(batteryStatusText(batteryInfo.status)); + // } + // }; + + // deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); + // deviceDetails += tr("VendorId:\t%1\n").arg(hexId(dc->deviceId().vendorId)); + // deviceDetails += tr("ProductId:\t%1\n").arg(hexId(dc->deviceId().productId)); + // deviceDetails += tr("Phys:\t\t%1\n").arg(dc->deviceId().phys); + // deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); + // deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); + // if (hasBattery && isOnline) deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); + // if (hasHIDPP && !isOnline) deviceDetails += tr("\n\n\t Device not active. Press any key on device to update.\n"); + // if (hasHIDPP && isOnline){ + // deviceDetails += "\n"; + // deviceDetails += tr("HID++ Version:\t%1\n").arg(HIDPPversion); + // deviceDetails += tr("HID++ Features:\t%1\n").arg(HIDPPfeatureText.join(",\n\t\t")); + // } + + // return deviceDetails; + // }; + + // QTimer::singleShot(200, this, [updateBatteryInfo](){updateBatteryInfo();}); + // if (m_deviceDetailsTextEdit) { + // QTimer::singleShot(1000, this, [this, getDeviceDetails](){m_deviceDetailsTextEdit->setText(getDeviceDetails());}); + // } } // ------------------------------------------------------------------------------------------------- diff --git a/src/extra-devices.cc.in b/src/extra-devices.cc.in index a69d3a8b..cbe95713 100644 --- a/src/extra-devices.cc.in +++ b/src/extra-devices.cc.in @@ -1,5 +1,6 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md + #include "devicescan.h" #include diff --git a/src/hidpp.cc b/src/hidpp.cc index f69c5d30..f2e2f870 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -70,6 +70,7 @@ const char* toString(HidppConnectionInterface::MsgResult res) ENUM_CASE_STRINGIFY(MsgResult::WriteError); ENUM_CASE_STRINGIFY(MsgResult::Timeout); ENUM_CASE_STRINGIFY(MsgResult::HidppError); + ENUM_CASE_STRINGIFY(MsgResult::FeatureNotSupported); } return "MsgResult::(unknown)"; } @@ -348,7 +349,7 @@ void FeatureSet::getFeatureIndex(FeatureCode fc, std::functionsendRequest(std::move(featureIndexReqMsg), - [cb=std::move(cb), fc, fcLSB, fcMSB](MsgResult result, Message msg) + [cb=std::move(cb), fc, fcLSB, fcMSB](MsgResult result, Message&& msg) { logDebug(hid) << tr("getFeatureIndex(%1) => %2, %3") .arg(to_integral(fc)).arg(toString(result)).arg(msg[4]); @@ -372,7 +373,7 @@ void FeatureSet::getFeatureCount(std::functionsendRequest(std::move(featureCountReqMsg), - [featureIndex, cb=std::move(cb)](MsgResult result, Message msg) { + [featureIndex, cb=std::move(cb)](MsgResult result, Message&& msg) { if (cb) cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); }); })); @@ -393,7 +394,7 @@ void FeatureSet::getFirmwareCount(std::functionsendRequest(std::move(fwCountReqMsg), - [featureIndex, cb=std::move(cb)](MsgResult result, Message msg) + [featureIndex, cb=std::move(cb)](MsgResult result, Message&& msg) { logDebug(hid) << tr("getFirmwareCount() => %1, featureIndex = %2, count = %3") .arg(toString(result)).arg(featureIndex).arg(msg[4]); @@ -404,7 +405,7 @@ void FeatureSet::getFirmwareCount(std::function cb) + std::function cb) { if (m_connection == nullptr) { @@ -416,13 +417,13 @@ void FeatureSet::getFirmwareInfo(uint8_t fwIndex, uint8_t entity, Message::Data{entity}); m_connection->sendRequest(std::move(fwVerReqMessage), - [cb=std::move(cb)](MsgResult res, Message msg) { + [cb=std::move(cb)](MsgResult res, Message&& msg) { if (cb) cb(res, FirmwareInfo(std::move(msg))); }); } // ------------------------------------------------------------------------------------------------- -void FeatureSet::getMainFirmwareInfo(std::function cb) +void FeatureSet::getMainFirmwareInfo(std::function cb) { getFirmwareCount(makeSafeCallback( [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) @@ -438,10 +439,10 @@ void FeatureSet::getMainFirmwareInfo(std::function cb) + std::function cb) { getFirmwareInfo(fwIndex, current, makeSafeCallback( - [this, current, max, fwIndex, cb=std::move(cb)](MsgResult res, FirmwareInfo fi) + [this, current, max, fwIndex, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) { logDebug(hid) << tr("getFirmwareInfo(%1, %2, %3) => %4, fi.type = %5, fi.ver = %6, fi.pref = %7") .arg(fwIndex).arg(max).arg(current).arg(toString(res)) @@ -475,7 +476,7 @@ void FeatureSet::initFromDevice(std::function cb) setState(State::Initializing); - getMainFirmwareInfo(makeSafeCallback([this, cb=std::move(cb)](MsgResult res, FirmwareInfo fi) + getMainFirmwareInfo(makeSafeCallback([this, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) { logDebug(hid) << tr("getMainFirmwareInfo() => %1, fi.type = %2").arg(toString(res)) .arg(to_integral(fi.firmwareType())); @@ -501,7 +502,7 @@ void FeatureSet::initFromDevice(std::function cb) } getFeatureIds(featureIndex, count, makeSafeCallback( - [this, cb=std::move(cb)](MsgResult res, FeatureTable ft) + [this, cb=std::move(cb)](MsgResult res, FeatureTable&& ft) { if (res != MsgResult::Ok) { setState(State::Error); @@ -521,7 +522,7 @@ void FeatureSet::initFromDevice(std::function cb) // ------------------------------------------------------------------------------------------------- void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, - std::function cb) + std::function cb) { if (m_connection == nullptr) { @@ -541,30 +542,27 @@ void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, for (uint8_t featureIndex = 1; featureIndex <= count; ++featureIndex) { // featureIdReqMsg[Offset::Payload] = featureIndex; - batch.emplace( - HidppConnectionInterface::RequestBatchItem { - Message(Message::Type::Long, DeviceIndex::WirelessDevice1, featureSetIndex, 1, - Message::Data{featureIndex}), - makeSafeCallback([featureTable, featureIndex](MsgResult res, Message msg) - { - if (res != MsgResult::Ok) return; - const uint16_t featureCode = (static_cast(msg[4]) << 8) - | static_cast(msg[5]); - const uint8_t featureType = msg[6]; - const bool softwareHidden = (featureType & (1<<6)); - const bool obsoleteFeature = (featureType & (1<<7)); - if (!softwareHidden && !obsoleteFeature) { - // logDebug(hid) << tr("featureCode %1 -> index %2").arg(featureCode).arg(featureIndex); - featureTable->emplace(featureCode, featureIndex); - } - })} - ); + batch.emplace(HidppConnectionInterface::RequestBatchItem { + Message(Message::Type::Long, DeviceIndex::WirelessDevice1, featureSetIndex, 1, + Message::Data{featureIndex}), + [featureTable, featureIndex](MsgResult res, Message&& msg) + { + if (res != MsgResult::Ok) return; + const uint16_t featureCode = (static_cast(msg[4]) << 8) + | static_cast(msg[5]); + const uint8_t featureType = msg[6]; + const bool softwareHidden = (featureType & (1<<6)); + const bool obsoleteFeature = (featureType & (1<<7)); + if (!softwareHidden && !obsoleteFeature) { + // logDebug(hid) << tr("featureCode %1 -> index %2").arg(featureCode).arg(featureIndex); + featureTable->emplace(featureCode, featureIndex); + } + } + }); } m_connection->sendRequestBatch(std::move(batch), - [featureTable, cb=std::move(cb)](std::vector results) - { - + [featureTable, cb=std::move(cb)](std::vector&& results) { if (cb) cb(results.back(), std::move(*featureTable)); }); } @@ -579,10 +577,11 @@ bool FeatureSet::featureCodeSupported(FeatureCode fc) const // ------------------------------------------------------------------------------------------------- uint8_t FeatureSet::featureIndex(FeatureCode fc) const { - if (!featureCodeSupported(fc)) return 0x00; - - const auto featureInfo = m_featureTable.find(to_integral(fc)); - return featureInfo->second; + const auto it = m_featureTable.find(to_integral(fc)); + if (it == m_featureTable.cend()) { + return 0x00; + } + return it->second; } // ================================================================================================= @@ -659,3 +658,44 @@ const char* toString(HIDPP::FeatureSet::State s) }; return "State::(unknown)"; } + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::FeatureCode fc) +{ + using FeatureCode = HIDPP::FeatureCode; + switch (fc) + { + ENUM_CASE_STRINGIFY(FeatureCode::Root); + ENUM_CASE_STRINGIFY(FeatureCode::FeatureSet); + ENUM_CASE_STRINGIFY(FeatureCode::FirmwareVersion); + ENUM_CASE_STRINGIFY(FeatureCode::DeviceName); + ENUM_CASE_STRINGIFY(FeatureCode::Reset); + ENUM_CASE_STRINGIFY(FeatureCode::DFUControlSigned); + ENUM_CASE_STRINGIFY(FeatureCode::BatteryStatus); + ENUM_CASE_STRINGIFY(FeatureCode::PresenterControl); + ENUM_CASE_STRINGIFY(FeatureCode::Sensor3D); + ENUM_CASE_STRINGIFY(FeatureCode::ReprogramControlsV4); + ENUM_CASE_STRINGIFY(FeatureCode::WirelessDeviceStatus); + ENUM_CASE_STRINGIFY(FeatureCode::SwapCancelButton); + ENUM_CASE_STRINGIFY(FeatureCode::PointerSpeed); + }; + return "FeatureCode::(unknown)"; +} + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::BatteryStatus bs) +{ + using BatteryStatus = HIDPP::BatteryStatus; + switch (bs) + { + ENUM_CASE_STRINGIFY(BatteryStatus::AlmostFull); + ENUM_CASE_STRINGIFY(BatteryStatus::Charging); + ENUM_CASE_STRINGIFY(BatteryStatus::ChargingError); + ENUM_CASE_STRINGIFY(BatteryStatus::Discharging); + ENUM_CASE_STRINGIFY(BatteryStatus::Full); + ENUM_CASE_STRINGIFY(BatteryStatus::InvalidBattery); + ENUM_CASE_STRINGIFY(BatteryStatus::SlowCharging); + ENUM_CASE_STRINGIFY(BatteryStatus::ThermalError); + }; + return "BatteryStatus::(unknown)"; +} diff --git a/src/hidpp.h b/src/hidpp.h index 0b1e7eb5..667e80c4 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -73,6 +73,26 @@ namespace HIDPP { constexpr uint8_t GetLongRegister = 0x83; } + // ------------------------------------------------------------------------------------------------- + // Battery Status as returned on HID++ BatteryStatus feature code (0x1000) + enum class BatteryStatus : uint8_t { + Discharging = 0x00, + Charging = 0x01, + AlmostFull = 0x02, + Full = 0x03, + SlowCharging = 0x04, + InvalidBattery = 0x05, + ThermalError = 0x06, + ChargingError = 0x07 + }; + + struct BatteryInfo + { + uint8_t currentLevel = 0; + uint8_t nextReportedLevel = 0; + BatteryStatus status = BatteryStatus::Discharging; + }; + // ----------------------------------------------------------------------------------------------- struct ProtocolVersion { uint8_t major = 0; @@ -197,10 +217,11 @@ class HidppConnectionInterface WriteError, Timeout, HidppError, + FeatureNotSupported, }; using SendResultCallback = std::function; - using RequestResultCallback = std::function; + using RequestResultCallback = std::function; virtual BusType busType() const = 0; @@ -220,7 +241,7 @@ class HidppConnectionInterface }; using RequestBatch = std::queue; - using RequestBatchResultCallback = std::function)>; + using RequestBatchResultCallback = std::function&&)>; virtual void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, bool continueOnError = false) = 0; @@ -230,17 +251,13 @@ class HidppConnectionInterface }; using DataBatch = std::queue; - using DataBatchResultCallback = std::function)>; + using DataBatchResultCallback = std::function&&)>; virtual void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, bool continueOnError = false) = 0; }; namespace HIDPP { // ----------------------------------------------------------------------------------------------- - namespace Bytes { - constexpr uint8_t SHORT_WIRELESS_NOTIFICATION_CODE = 0x41; // TODO MOVE RENAME /// - } - class FirmwareInfo { public: @@ -287,8 +304,6 @@ namespace HIDPP { bool featureCodeSupported(FeatureCode fc) const; auto featureCount() const { return m_featureTable.size(); } - //void populateFeatureTable(); - signals: void stateChanged(State s); @@ -300,12 +315,12 @@ namespace HIDPP { void getFeatureCount(std::function cb); void getFirmwareCount(std::function cb); void getFeatureIds(uint8_t featureSetIndex, uint8_t count, - std::function cb); - void getMainFirmwareInfo(std::function cb); + std::function cb); + void getMainFirmwareInfo(std::function cb); void getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t current, - std::function cb); + std::function cb); void getFirmwareInfo(uint8_t fwIndex, uint8_t entity, - std::function cb); + std::function cb); void setState(State s); @@ -317,6 +332,8 @@ namespace HIDPP { }; } //end namespace HIDPP -const char* toString(HIDPP::Error e); const char* toString(HidppConnectionInterface::MsgResult r); +const char* toString(HIDPP::Error e); const char* toString(HIDPP::FeatureSet::State s); +const char* toString(HIDPP::FeatureCode fc); +const char* toString(HIDPP::BatteryStatus bs); \ No newline at end of file diff --git a/src/iconwidgets.cc b/src/iconwidgets.cc index 1c034b23..bffeb31d 100644 --- a/src/iconwidgets.cc +++ b/src/iconwidgets.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "iconwidgets.h" namespace { diff --git a/src/iconwidgets.h b/src/iconwidgets.h index 7e7330f6..84a703e2 100644 --- a/src/iconwidgets.h +++ b/src/iconwidgets.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include "projecteur-icons-def.h" diff --git a/src/imageitem.cc b/src/imageitem.cc index 3c185a72..ee124d6a 100644 --- a/src/imageitem.cc +++ b/src/imageitem.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "imageitem.h" #include diff --git a/src/imageitem.h b/src/imageitem.h index 104cf951..aa164473 100644 --- a/src/imageitem.h +++ b/src/imageitem.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index f092dfbe..7c09240d 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "inputmapconfig.h" #include "actiondelegate.h" diff --git a/src/inputmapconfig.h b/src/inputmapconfig.h index ea2f96a8..6044e4a3 100644 --- a/src/inputmapconfig.h +++ b/src/inputmapconfig.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/ +// - See LICENSE.md and README.md #pragma once #include "deviceinput.h" diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 81234ec1..52b102f4 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "inputseqedit.h" #include "deviceinput.h" diff --git a/src/inputseqedit.h b/src/inputseqedit.h index f0766dd7..c19fe32c 100644 --- a/src/inputseqedit.h +++ b/src/inputseqedit.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include "deviceinput.h" diff --git a/src/linuxdesktop.cc b/src/linuxdesktop.cc index f3cbf133..2a0f588d 100644 --- a/src/linuxdesktop.cc +++ b/src/linuxdesktop.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "linuxdesktop.h" #include "logging.h" @@ -27,8 +29,8 @@ namespace { QStringLiteral("/org/gnome/Shell/Screenshot"), QStringLiteral("org.gnome.Shell.Screenshot")); QDBusReply reply = interface.call(QStringLiteral("Screenshot"), false, false, filepath); - - if (reply.value()) + + if (reply.value()) { QPixmap pm(filepath); QFile::remove(filepath); @@ -66,7 +68,7 @@ namespace { QPixmap pm(QApplication::primaryScreen()->grabWindow( QApplication::desktop()->winId(), g.x(), g.y(), g.width(), g.height())); - if (!pm.isNull()) + if (!pm.isNull()) { pm.setDevicePixelRatio(screen->devicePixelRatio()); return pm.copy(screen->geometry()); @@ -87,7 +89,7 @@ LinuxDesktop::LinuxDesktop(QObject* parent) const auto xdgCurrentDesktop = env.value(QStringLiteral("XDG_CURRENT_DESKTOP")); if (gnomeSessionId.size() || xdgCurrentDesktop.contains("Gnome", Qt::CaseInsensitive)) { m_type = LinuxDesktop::Type::Gnome; - } + } else if (kdeFullSession.size() || desktopSession == "kde-plasma") { m_type = LinuxDesktop::Type::KDE; } @@ -96,17 +98,17 @@ LinuxDesktop::LinuxDesktop(QObject* parent) { // check for wayland session const auto waylandDisplay = env.value(QStringLiteral("WAYLAND_DISPLAY")); const auto xdgSessionType = env.value(QStringLiteral("XDG_SESSION_TYPE")); - m_wayland = (xdgSessionType == "wayland") + m_wayland = (xdgSessionType == "wayland") || waylandDisplay.contains("wayland", Qt::CaseInsensitive); } } QPixmap LinuxDesktop::grabScreen(QScreen* screen) const { - if (screen == nullptr) + if (screen == nullptr) return QPixmap(); - - if (isWayland()) + + if (isWayland()) return grabScreenWayland(screen); #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) @@ -117,7 +119,7 @@ QPixmap LinuxDesktop::grabScreen(QScreen* screen) const if (isVirtualDesktop) return grabScreenVirtualDesktop(screen); - + // everything else.. usually X11 return screen->grabWindow(0); } @@ -126,13 +128,13 @@ QPixmap LinuxDesktop::grabScreenWayland(QScreen* screen) const { #if HAS_Qt5_DBus QPixmap pm; - switch (type()) + switch (type()) { - case LinuxDesktop::Type::Gnome: - pm = grabScreenDBusGnome(); + case LinuxDesktop::Type::Gnome: + pm = grabScreenDBusGnome(); break; - case LinuxDesktop::Type::KDE: - pm = grabScreenDBusKde(); + case LinuxDesktop::Type::KDE: + pm = grabScreenDBusKde(); break; default: logWarning(desktop) << tr("Currently zoom on Wayland is only supported via DBus on KDE and GNOME."); diff --git a/src/linuxdesktop.h b/src/linuxdesktop.h index c29dca9b..1c9048a1 100644 --- a/src/linuxdesktop.h +++ b/src/linuxdesktop.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include @@ -6,7 +7,7 @@ class QScreen; -class LinuxDesktop : public QObject +class LinuxDesktop : public QObject { Q_OBJECT diff --git a/src/logging.cc b/src/logging.cc index 46e4ee93..d3f86aec 100644 --- a/src/logging.cc +++ b/src/logging.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "logging.h" #include diff --git a/src/logging.h b/src/logging.h index 7feed3f9..e9552320 100644 --- a/src/logging.h +++ b/src/logging.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/Projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/Projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/main.cc b/src/main.cc index 36cf596b..7775c7aa 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "projecteurapp.h" #include "projecteur-GitVersion.h" diff --git a/src/nativekeyseqedit.cc b/src/nativekeyseqedit.cc index 898fbe6c..46c4ce48 100644 --- a/src/nativekeyseqedit.cc +++ b/src/nativekeyseqedit.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "nativekeyseqedit.h" #include "inputmapconfig.h" diff --git a/src/nativekeyseqedit.h b/src/nativekeyseqedit.h index 0b035bb8..f0b04305 100644 --- a/src/nativekeyseqedit.h +++ b/src/nativekeyseqedit.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once // _Note_: This is custom implementation similar to QKeySequenceEdit. Unfortunately QKeySequence diff --git a/src/preferencesdlg.cc b/src/preferencesdlg.cc index 73084783..cdf36259 100644 --- a/src/preferencesdlg.cc +++ b/src/preferencesdlg.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "preferencesdlg.h" #include "projecteur-GitVersion.h" // auto generated version information diff --git a/src/preferencesdlg.h b/src/preferencesdlg.h index 41c3348e..e1159312 100644 --- a/src/preferencesdlg.h +++ b/src/preferencesdlg.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/projecteur-icons-def.h b/src/projecteur-icons-def.h index 28936555..44fe3241 100644 --- a/src/projecteur-icons-def.h +++ b/src/projecteur-icons-def.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once // Auto generated defines for icon-font with `fontcustom` diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index 16f947fb..a13fcb72 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "projecteurapp.h" #include "aboutdlg.h" diff --git a/src/projecteurapp.h b/src/projecteurapp.h index 487a8b70..05efd117 100644 --- a/src/projecteurapp.h +++ b/src/projecteurapp.h @@ -1,5 +1,7 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once + #include "spotlight.h" #include diff --git a/src/settings.cc b/src/settings.cc index 5e8e3607..dec74341 100644 --- a/src/settings.cc +++ b/src/settings.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "settings.h" #include "device.h" diff --git a/src/settings.h b/src/settings.h index c9c4b83e..f5cc11e4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md # pragma once #include diff --git a/src/spotlight.cc b/src/spotlight.cc index 14cdbdd1..ef36007b 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "spotlight.h" #include "device.h" @@ -144,32 +146,7 @@ int Spotlight::connectDevices() { if (dc->hasHidppSupport()) { - auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc); - if (hidppCon) - { - // connect to hidpp sub connection signals - connect(&*hidppCon, &SubHidppConnection::receivedBatteryInfo, - dc.get(), &DeviceConnection::setBatteryInfo); - // auto hidppActivated = [this, dc]() { - // if (std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), - // dc->deviceId()) == m_activeDeviceIds.cend()) { - // logInfo(device) << dc->deviceName() << "is now active."; - // m_activeDeviceIds.emplace_back(dc->deviceId()); - // emit deviceActivated(dc->deviceId(), dc->deviceName()); - // } - // }; - // auto hidppDeactivated = [this, dc]() { - // auto it = std::find(m_activeDeviceIds.cbegin(), m_activeDeviceIds.cend(), dc->deviceId()); - // if (it != m_activeDeviceIds.cend()) { - // logInfo(device) << dc->deviceName() << "is deactivated."; - // m_activeDeviceIds.erase(it); - // emit deviceDeactivated(dc->deviceId(), dc->deviceName()); - // } - // }; - // connect(&*hidppCon, &SubHidppConnection::activated, dc.get(), hidppActivated); - // connect(&*hidppCon, &SubHidppConnection::deactivated, dc.get(), hidppDeactivated); - // connect(&*hidppCon, &SubHidppConnection::destroyed, dc.get(), hidppDeactivated); - + if (auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc)) { return hidppCon; } } diff --git a/src/spotlight.h b/src/spotlight.h index c5a01c89..edd8176b 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/spotshapes.cc b/src/spotshapes.cc index 4c1c2168..60b067af 100644 --- a/src/spotshapes.cc +++ b/src/spotshapes.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "spotshapes.h" #include diff --git a/src/spotshapes.h b/src/spotshapes.h index 70e8087a..54345a48 100644 --- a/src/spotshapes.h +++ b/src/spotshapes.h @@ -1,4 +1,5 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md #pragma once #include diff --git a/src/virtualdevice.cc b/src/virtualdevice.cc index 658ea8d4..6e7b9ea1 100644 --- a/src/virtualdevice.cc +++ b/src/virtualdevice.cc @@ -1,4 +1,6 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + #include "virtualdevice.h" #include "logging.h" diff --git a/src/virtualdevice.h b/src/virtualdevice.h index 482ad543..e5b73f38 100644 --- a/src/virtualdevice.h +++ b/src/virtualdevice.h @@ -1,6 +1,7 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md -// Virtual Device to emit customized events from Projecteur device +// Virtual Device to emit custom events from Projecteur. // The spotlight.cc grabs mouse inputs from Logitech Spotlight device. // This module is used when the input events are supposed to be forwarded to the system. From 97e289ab24b63d14e755c88a176fffa3cd1c6d34 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 10 Sep 2021 22:07:35 +0200 Subject: [PATCH 065/110] Refactoring of hid++ functionality part 4. --- src/asynchronous.h | 4 +- src/device-hidpp.cc | 272 ++++++++++++++++++++++++++++++++++--------- src/device-hidpp.h | 27 +++-- src/device.cc | 25 ++-- src/device.h | 36 ++---- src/deviceswidget.cc | 10 +- src/deviceswidget.h | 1 - src/hidpp.cc | 14 ++- src/hidpp.h | 28 ++++- src/spotlight.cc | 125 +++++++------------- src/spotlight.h | 4 - 11 files changed, 342 insertions(+), 204 deletions(-) diff --git a/src/asynchronous.h b/src/asynchronous.h index cbf30912..d310dd41 100644 --- a/src/asynchronous.h +++ b/src/asynchronous.h @@ -144,8 +144,8 @@ class Async protected: /// Returns a function object that is guaranteed to be invoked in the own thread context. template - auto makeSafeCallback(F&& f) { - return async::makeSafeCallback(static_cast(this), std::forward(f)); + auto makeSafeCallback(F&& f, bool forceQueued = true) { + return async::makeSafeCallback(static_cast(this), std::forward(f), forceQueued); } /// Post a function to the own event loop. diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index 8e957b56..74fbbe71 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -16,10 +16,9 @@ DECLARE_LOGGING_CATEGORY(hid) // ------------------------------------------------------------------------------------------------- SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token, - const DeviceScan::SubDevice& sd, const DeviceId& id) - : SubHidrawConnection(token, sd) + const DeviceId& id, const DeviceScan::SubDevice& sd) + : SubHidrawConnection(token, id, sd) , m_featureSet(this) - , m_busType(id.busType) , m_requestCleanupTimer(new QTimer(this)) { m_requestCleanupTimer->setInterval(500); @@ -63,7 +62,7 @@ ssize_t SubHidppConnection::sendData(std::vector data) { } // ------------------------------------------------------------------------------------------------- -ssize_t SubHidppConnection::sendData(HIDPP::Message msg) // +ssize_t SubHidppConnection::sendData(HIDPP::Message msg) { constexpr ssize_t errorResult = -1; if (!msg.isValid()) { @@ -78,7 +77,7 @@ ssize_t SubHidppConnection::sendData(HIDPP::Message msg) // // 2. Long (20 byte long starting with 0x11) // However, the bluetooth connection only accepts data in long (20 byte) messages. - if (m_busType == BusType::Bluetooth) + if (busType() == BusType::Bluetooth) { if (msg.deviceIndex() == HIDPP::DeviceIndex::DefaultDevice) { logWarn(hid) << tr("Invalid message device index in data '%1' for device connected " @@ -99,7 +98,8 @@ void SubHidppConnection::sendData(std::vector data, SendResultCallback } // ------------------------------------------------------------------------------------------------- -void SubHidppConnection::sendData(HIDPP::Message msg, SendResultCallback resultCb) { +void SubHidppConnection::sendData(HIDPP::Message msg, SendResultCallback resultCb) +{ postSelf([this, msg = std::move(msg), cb = std::move(resultCb)]() mutable { // Check for valid message format if (!msg.isValid()) { @@ -107,7 +107,7 @@ void SubHidppConnection::sendData(HIDPP::Message msg, SendResultCallback resultC return; } - if (m_busType == BusType::Bluetooth) { + if (busType() == BusType::Bluetooth) { // For bluetooth always convert to a long message if we have a short message msg.convertToLong(); } @@ -154,7 +154,7 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r return; } - if (m_busType == BusType::Bluetooth) { + if (busType() == BusType::Bluetooth) { // For bluetooth always convert to a long message if we have a short message msg.convertToLong(); } @@ -176,7 +176,7 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r if (it->callBack) { it->callBack(result, HIDPP::Message()); } m_requests.erase(it); - })); + }, false)); // Place request in request list with a timeout m_requests.emplace_back(RequestEntry{ @@ -232,7 +232,7 @@ void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallb // continue processing the rest of the batch sendDataBatch(std::move(batch), std::move(batchCb), coe, std::move(results)); - })); + }, false)); }); } @@ -280,17 +280,73 @@ void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatc // continue processing the rest of the batch sendRequestBatch(std::move(batch), std::move(batchCb), coe, std::move(results)); - })); + }, false)); + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::registerNotificationCallback(QObject* obj, uint8_t featureIndex, + NotificationCallback cb, uint8_t function) +{ + if (obj == nullptr || !cb) return; + + postSelf([this, obj, featureIndex, function, cb=std::move(cb)]() + { + auto& callbackList = m_notificationSubscribers[featureIndex]; + callbackList.emplace_back(Subscriber{obj, function, std::move(cb)}); + + if (obj != this) + { + connect(obj, &QObject::destroyed, this, [this, obj, featureIndex, function]() + { + auto& callbackList = m_notificationSubscribers[featureIndex]; + callbackList.remove_if([obj, function](const Subscriber& item){ + return (item.object == obj && item.function == function); + }); + }); + } }); } +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::registerNotificationCallback(QObject* obj, HIDPP::Notification n, + NotificationCallback cb, uint8_t function) +{ + registerNotificationCallback(obj, to_integral(n), std::move(cb), function); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::unregisterNotificationCallback(QObject* obj, + uint8_t featureIndex, + uint8_t function) +{ + postSelf([this, obj, featureIndex, function](){ + auto& callbackList = m_notificationSubscribers[featureIndex]; + callbackList.remove_if([obj, function](const Subscriber& item){ + if (item.object == obj) { + if (function > 15) return true; + if (item.function == function) return true; + } + return false; + }); + }); +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::unregisterNotificationCallback(QObject* obj, + HIDPP::Notification n, + uint8_t function) +{ + unregisterNotificationCallback(obj, to_integral(n), function); +} + // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidppConnection::create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc) { const int devfd = openHidrawSubDevice(sd, dc.deviceId()); if (devfd == -1) return std::shared_ptr(); - auto connection = std::make_shared(Token{}, sd, dc.deviceId()); + auto connection = std::make_shared(Token{}, dc.deviceId(), sd); if (dc.hasHidppSupport()) connection->m_details.deviceFlags |= DeviceFlag::Hidpp; connection->createSocketNotifiers(devfd); @@ -335,23 +391,29 @@ void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length, } // ------------------------------------------------------------------------------------------------- -void SubHidppConnection::queryBatteryStatus() +void SubHidppConnection::getBatteryLevelStatus( + std::function cb) { - // TODO refactor battery status handling - // if (hasFlags(DeviceFlag::ReportBattery)) { - // const uint8_t batteryFeatureIndex = - // m_featureSet.getFeatureIndex(HIDPP::FeatureCode::BatteryStatus); - // if (batteryFeatureIndex) { - // const uint8_t batteryCmd[] = {HIDPP::Bytes::SHORT_MSG, - // HIDPP::Bytes::MSG_TO_SPOTLIGHT, - // batteryFeatureIndex, - // m_featureSet.getRandomFunctionCode(0x00), - // 0x00, - // 0x00, - // 0x00}; - // sendData(batteryCmd, sizeof(batteryCmd)); - // } - // } + using namespace HIDPP; + + const auto batteryIndex = m_featureSet.featureIndex(FeatureCode::BatteryStatus); + if (batteryIndex == 0) + { + if (cb) cb(MsgResult::FeatureNotSupported, {}); + return; + } + + Message batteryReqMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, batteryIndex, 0); + sendRequest(std::move(batteryReqMsg), [cb=std::move(cb)](MsgResult res, Message&& msg) + { + if (!cb) return; + + auto batteryInfo = (res == MsgResult::Ok) ? BatteryInfo{} + : BatteryInfo{msg[4], + msg[5], + to_enum(msg[6])}; + cb(res, std::move(batteryInfo)); + }); } // ------------------------------------------------------------------------------------------------- @@ -412,7 +474,7 @@ void SubHidppConnection::initReceiver(std::function cb) setReceiverState(ReceiverState::Initializing); - if (m_busType != BusType::Usb) + if (busType() != BusType::Usb) { // If bus type is not USB return immediately with success result and initialized state setReceiverState(ReceiverState::Initialized); @@ -429,50 +491,50 @@ void SubHidppConnection::initReceiver(std::function cb) RequestBatchItem{ // Reset device: get rid of any device configuration by other programs Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 0, {}), - makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + [index=++index](MsgResult result, HIDPP::Message /* msg */) { if (result == MsgResult::Ok) return; logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); - }) + } }, RequestBatchItem{ // Turn off software bit and keep the wireless notification bit on Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x01, 0x00}), - makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + [index=++index](MsgResult result, HIDPP::Message /* msg */) { if (result == MsgResult::Ok) return; logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); - }) + } }, RequestBatchItem{ // Initialize USB dongle Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 2, {}), - makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + [index=++index](MsgResult result, HIDPP::Message /* msg */) { if (result == MsgResult::Ok) return; logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); - }) + } }, RequestBatchItem{ // --- Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 2, {0x02, 0x00, 0x00}), - makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + [index=++index](MsgResult result, HIDPP::Message /* msg */) { if (result == MsgResult::Ok) return; logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); - }) + } }, RequestBatchItem{ // Now enable both software and wireless notification bit Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x09, 0x00}), - makeSafeCallback([index=++index](MsgResult result, HIDPP::Message /* msg */) { + [index=++index](MsgResult result, HIDPP::Message /* msg */) { if (result == MsgResult::Ok) return; logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); - }) + } }, }}; @@ -482,7 +544,7 @@ void SubHidppConnection::initReceiver(std::function cb) setReceiverState(results.back() == MsgResult::Ok ? ReceiverState::Initialized : ReceiverState::Error); if (cb) cb(m_receiverState); - })); + }, false)); }); } @@ -492,8 +554,7 @@ void SubHidppConnection::initPresenter(std::function cb) postSelf([this, cb=std::move(cb)](){ if (m_presenterState == PresenterState::Initializing || m_presenterState == PresenterState::Initialized_Offline - || m_presenterState == PresenterState::Initialized_Online - || m_presenterState == PresenterState::Uninitialized_Offline) + || m_presenterState == PresenterState::Initialized_Online) { logDebug(hid) << "Cannot init presenter when offline, initializing or already initialized."; if (cb) cb(m_presenterState); @@ -523,6 +584,7 @@ void SubHidppConnection::initPresenter(std::function cb) logDebug(hid) << tr("Received %1 supported features from device. (%2)") .arg(m_featureSet.featureCount()).arg(path()); + registerForFeatureNotifications(); updateDeviceFlags(); initFeatures(makeSafeCallback( [this, cb=std::move(cb)](std::map&& resultMap) @@ -534,12 +596,12 @@ void SubHidppConnection::initPresenter(std::function cb) } setPresenterState(PresenterState::Initialized_Online); if (cb) cb(m_presenterState); - })); + }, false)); return; } } if (cb) cb(m_presenterState); - })); + }, false)); }); } @@ -659,21 +721,104 @@ void SubHidppConnection::updateDeviceFlags() setFlags(featureFlagsSet, true); } +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::registerForFeatureNotifications() +{ + using namespace HIDPP; + + // Logitech button next and back press and hold + movement + if (const auto rcIndex = m_featureSet.featureIndex(FeatureCode::ReprogramControlsV4)) + { + registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) + { + // TODO implement button hold states + // Logitech Spotlight: + // * Next Button = 0xda + // * Back Button = 0xdc + // Byte 5 and 7 indicate pressed buttons + // Back and next can be pressed at the same time + + constexpr uint8_t ButtonNext = 0xda; + constexpr uint8_t ButtonBack = 0xdc; + const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; + const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; + logDebug(hid) << tr("Buttons pressed: Next = %1, Back = %2") + .arg(isNextPressed).arg(isBackPressed) << msg.hex(); + + }, false), 0 /* function 0 */); + + registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) + { + // TODO Implement hold and move logic and bindings + Q_UNUSED(msg); + + // byte 4 : -1 for left movement, 0 for right movement + // byte 5 : horizontal movement speed -128 to 127 + // byte 6 : -1 for up movement, 0 for down movement + // byte 7 : vertical movement speed -128 to 127 + + // auto cast = [](uint8_t v) -> int{ return static_cast(v); }; + // logDebug(hid) << tr("4 = %1, 5 = %2, 6 = %3, 7 = %4") + // .arg(cast(msg[4]), 4) + // .arg(cast(msg[5]), 4) + // .arg(cast(msg[6]), 4) + // .arg(cast(msg[7]), 4); + }, false), 1 /* function 1 */); + } + + if (const auto batIndex = m_featureSet.featureIndex(FeatureCode::BatteryStatus)) + { + // TODO register for BatteryLevelStatusBroadcastEvent + // A device can send a battery status spontaneously to the software. + } +} + +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::registerForUsbNotifications() +{ + // Register for device connection notifications from the usb receiver + registerNotificationCallback(this, HIDPP::Notification::DeviceConnection, makeSafeCallback( + [this](HIDPP::Message&& msg) + { + const bool linkEstablished = !static_cast(msg[4] & (1<<6)); + logDebug(hid) << tr("%1, link established = %2") + .arg(toString(HIDPP::Notification::DeviceConnection)).arg(linkEstablished); + + if (!linkEstablished) { + // TODO no link to device => depending on current state set new presenter state + return; + } + + if (m_presenterState == PresenterState::Uninitialized_Offline + || m_presenterState == PresenterState::Initialized_Offline + || m_presenterState == PresenterState::Uninitialized + || m_presenterState == PresenterState::Error) + { + logInfo(hid) << tr("Device '%1' came online.").arg(path()); + checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState /* ps */) { + //... + }, false)); + } + }, false)); +} + // ------------------------------------------------------------------------------------------------- void SubHidppConnection::subDeviceInit() { if (!hasFlags(DeviceFlag::Hidpp)) return; + registerForUsbNotifications(); + // Init receiver - will return almost immediately for bluetooth connections initReceiver(makeSafeCallback([this](ReceiverState rs) { Q_UNUSED(rs); // Independent of the receiver init result, try to initialize the // presenter device HID++ features and more - checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState ps) { - logDebug(hid) << tr("subDeviceInit, checkAndUpdatePresenterState = %1").arg(toString(ps)); - })); - })); + checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState /* ps */) { + //... + }, false)); + }, false)); } // ------------------------------------------------------------------------------------------------- @@ -783,13 +928,26 @@ void SubHidppConnection::checkAndUpdatePresenterState(std::function&& resultMap) + { + if (resultMap.size()) { + for (const auto& res : resultMap) { + logDebug(hid) << tr("InitFeature result %1 => %2").arg(toString(res.first)).arg(toString(res.second)); + } + } + setPresenterState(PresenterState::Initialized_Online); + if (cb) cb(m_presenterState); + }, false)); + } + else if (m_presenterState == PresenterState::Initialized_Online) { setPresenterState(PresenterState::Initialized_Online); if (cb) cb(m_presenterState); } - })); + }, false)); }); } @@ -858,14 +1016,18 @@ void SubHidppConnection::onHidppDataAvailable(int fd) else if (msg.softwareId() == 0 || msg.subId() < 0x80) { // Event/Notification - // TODO Notify listeners registered to the notification type - // check for wireless notification code 0x41 from usb dongle (see hid++ 1.0 docs) + // logDebug(hid) << tr("Received notification (%1) on %2").arg(msg.hex()).arg(path()); - logDebug(hid) << tr("Received notification (%1) on %2").arg(msg.hex()).arg(path()); + // Notify subscribers + const auto& callbackList = m_notificationSubscribers[msg.featureIndex()]; + for ( const auto& subscriber : callbackList) { + if (subscriber.function > 15 || subscriber.function == msg.function()) { + subscriber.cb(msg); + } + } } else { - // TODO check for device event messages, that don't require a request logWarn(hid) << tr("Received hidpp message " "'%1' without matching request.").arg(msg.hex()); } diff --git a/src/device-hidpp.h b/src/device-hidpp.h index 0ac62c51..699063dc 100644 --- a/src/device-hidpp.h +++ b/src/device-hidpp.h @@ -7,6 +7,7 @@ #include #include +#include class QTimer; @@ -32,15 +33,14 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); - SubHidppConnection(SubHidrawConnection::Token, const DeviceScan::SubDevice& sd, - const DeviceId& id); + SubHidppConnection(SubHidrawConnection::Token, const DeviceId&, const DeviceScan::SubDevice&); ~SubHidppConnection(); using SubHidrawConnection::sendData; // --- HidppConnectionInterface implementation: - BusType busType() const override { return m_busType; } + BusType busType() const override { return m_details.deviceId.busType; } ssize_t sendData(std::vector msg) override; ssize_t sendData(HIDPP::Message msg) override; void sendData(std::vector msg, SendResultCallback resultCb) override; @@ -52,6 +52,15 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, bool continueOnError = false) override; + void registerNotificationCallback(QObject* obj, HIDPP::Notification notification, + NotificationCallback cb, uint8_t function = 0xff) override; + void registerNotificationCallback(QObject* obj, uint8_t featureIndex, + NotificationCallback cb, uint8_t function = 0xff) override; + void unregisterNotificationCallback(QObject* obj, uint8_t featureIndex, + uint8_t function = 0xff) override; + void unregisterNotificationCallback(QObject* obj, HIDPP::Notification notification, + uint8_t function = 0xff) override; + // --- PresenterState presenterState() const; @@ -60,7 +69,7 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void sendPing(RequestResultCallback cb); HIDPP::ProtocolVersion protocolVersion() const; - void queryBatteryStatus(); // TODO refactory battery info handling + void getBatteryLevelStatus(std::function cb); void sendVibrateCommand(uint8_t intensity, uint8_t length, RequestResultCallback cb); /// Set device pointer speed - speed needs to be in the range [0-9] void setPointerSpeed(uint8_t speed, RequestResultCallback cb); @@ -69,13 +78,13 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void receiverStateChanged(ReceiverState); void presenterStateChanged(PresenterState); - void receivedBatteryInfo(QByteArray batteryData); - private: void subDeviceInit(); void initReceiver(std::function); void initPresenter(std::function); void updateDeviceFlags(); + void registerForUsbNotifications(); + void registerForFeatureNotifications(); /// Initializes features. Returns a map of initalized features and the result from it. void initFeatures(std::function&&)> cb); @@ -96,7 +105,6 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt bool continueOnError, std::vector results); HIDPP::FeatureSet m_featureSet; - const BusType m_busType = BusType::Unknown; HIDPP::ProtocolVersion m_protocolVersion; ReceiverState m_receiverState = ReceiverState::Uninitialized; @@ -104,13 +112,16 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt /// A request entry for request messages sent to the device. struct RequestEntry { - HIDPP::Message request; // bytes 0(or 1) to 5 should be enough to check against reply + HIDPP::Message request; std::chrono::time_point validUntil; RequestResultCallback callBack; }; std::list m_requests; QTimer* m_requestCleanupTimer = nullptr; + + struct Subscriber { QObject* object = nullptr; uint8_t function; NotificationCallback cb; }; + std::unordered_map> m_notificationSubscribers; }; const char* toString(SubHidppConnection::ReceiverState rs); diff --git a/src/device.cc b/src/device.cc index bd95d0fc..36586ae5 100644 --- a/src/device.cc +++ b/src/device.cc @@ -117,15 +117,15 @@ bool DeviceConnection::hasHidppSupport() const { // } // ------------------------------------------------------------------------------------------------- -SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, +SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode) - : type(type), mode(mode), devicePath(sd.deviceFile) + : deviceId(dId), type(type), mode(mode), devicePath(sd.deviceFile) {} // ------------------------------------------------------------------------------------------------- -SubDeviceConnection::SubDeviceConnection(const DeviceScan::SubDevice& sd, +SubDeviceConnection::SubDeviceConnection(const DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode) - : m_details(sd, type, mode) {} + : m_details(dId, sd, type, mode) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; @@ -159,11 +159,6 @@ void SubDeviceConnection::disconnect() { } } -// ------------------------------------------------------------------------------------------------- -void SubDeviceConnection::setReadNotifierEnabled(bool enabled) { - if (m_readNotifier) m_readNotifier->setEnabled(enabled); -} - // ------------------------------------------------------------------------------------------------- const std::shared_ptr& SubDeviceConnection::inputMapper() const { return m_inputMapper; @@ -175,8 +170,8 @@ QSocketNotifier* SubDeviceConnection::socketReadNotifier() { } // ------------------------------------------------------------------------------------------------- -SubEventConnection::SubEventConnection(Token, const DeviceScan::SubDevice& sd) - : SubDeviceConnection(sd, ConnectionType::Event, ConnectionMode::ReadOnly) {} +SubEventConnection::SubEventConnection(Token, const DeviceId& dId, const DeviceScan::SubDevice& sd) + : SubDeviceConnection(dId, sd, ConnectionType::Event, ConnectionMode::ReadOnly) {} // ------------------------------------------------------------------------------------------------- bool SubEventConnection::isConnected() const { @@ -215,7 +210,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: return std::shared_ptr(); } - auto connection = std::make_shared(Token{}, sd); + auto connection = std::make_shared(Token{}, dc.deviceId(), sd); if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; @@ -267,8 +262,8 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } // ------------------------------------------------------------------------------------------------- -SubHidrawConnection::SubHidrawConnection(Token, const DeviceScan::SubDevice& sd) - : SubDeviceConnection(sd, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} +SubHidrawConnection::SubHidrawConnection(Token, const DeviceId& dId, const DeviceScan::SubDevice& sd) + : SubDeviceConnection(dId, sd, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} // ------------------------------------------------------------------------------------------------- SubHidrawConnection::~SubHidrawConnection() = default; @@ -294,7 +289,7 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca const int devfd = openHidrawSubDevice(sd, dc.deviceId()); if (devfd == -1) return std::shared_ptr(); - auto connection = std::make_shared(Token{}, sd); + auto connection = std::make_shared(Token{}, dc.deviceId(), sd); connection->createSocketNotifiers(devfd); connect(connection->socketReadNotifier(), &QSocketNotifier::activated, diff --git a/src/device.h b/src/device.h index 48eecb84..ac5d9e03 100644 --- a/src/device.h +++ b/src/device.h @@ -42,12 +42,7 @@ class DeviceConnection : public QObject bool removeSubDevice(const QString& path); const auto& subDevices() { return m_subDeviceConnections; } - // // TODO ... battery status on device or subdevice level? - // void queryBatteryStatus(); - // auto getBatteryInfo(){ return m_batteryInfo; } - -// public slots: -// void setBatteryInfo(const QByteArray& batteryData); + // // TODO ... Refactor battery status handling signals: void subDeviceConnected(const DeviceId& id, const QString& path); @@ -86,9 +81,10 @@ ENUM(DeviceFlag, DeviceFlags) // ------------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { - SubDeviceConnectionDetails(const DeviceScan::SubDevice& sd, + SubDeviceConnectionDetails(const DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode); + DeviceId deviceId; ConnectionType type; ConnectionMode mode; bool grabbed = false; @@ -121,33 +117,25 @@ class SubDeviceConnection : public QObject, public async::Async& inputMapper() const; QSocketNotifier* socketReadNotifier(); // Read notifier for Hidraw and Event connections for receiving data from device - // // HID++ specific functions: These commands write on device and expect some return message - // // virtual bool isOnline() const { return false; }; - // // virtual void sendVibrateCommand(uint8_t intensity, uint8_t length); - // virtual void queryBatteryStatus(); - // virtual float getHIDppProtocol() const { return -1; }; - signals: void flagsChanged(DeviceFlags f); void socketReadError(int err); protected: - SubDeviceConnection(const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode); + SubDeviceConnection(const DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode); DeviceFlags setFlags(DeviceFlags f, bool set = true); SubDeviceConnectionDetails m_details; @@ -165,7 +153,7 @@ class SubEventConnection : public SubDeviceConnection static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); - SubEventConnection(Token, const DeviceScan::SubDevice& sd); + SubEventConnection(Token, const DeviceId&, const DeviceScan::SubDevice&); bool isConnected() const; auto& inputBuffer() { return m_inputEventBuffer; } @@ -193,7 +181,7 @@ class SubHidrawConnection : public SubDeviceConnection, public HidrawConnectionI static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); - SubHidrawConnection(Token, const DeviceScan::SubDevice& sd); + SubHidrawConnection(Token, const DeviceId&, const DeviceScan::SubDevice&); virtual ~SubHidrawConnection(); virtual bool isConnected() const override; virtual void disconnect() override; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 6620d118..5dee4fec 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -49,7 +49,6 @@ namespace { // ------------------------------------------------------------------------------------------------- DevicesWidget::DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent) : QWidget(parent) - , m_updateDeviceDetailsTimer(new QTimer(this)) { createDeviceComboBox(spotlight); @@ -258,14 +257,9 @@ QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) m_deviceDetailsTextEdit->setText(""); updateDeviceDetails(spotlight); - - connect(m_updateDeviceDetailsTimer, &QTimer::timeout, this, [this, spotlight](){updateDeviceDetails(spotlight);}); - m_updateDeviceDetailsTimer->start(900000); // Update every 15 minutes - connect(this, &DevicesWidget::currentDeviceChanged, this, [this, spotlight](){updateDeviceDetails(spotlight);}); - connect(spotlight, &Spotlight::deviceActivated, this, - [this, spotlight](const DeviceId& d){if (d==currentDeviceId()) updateDeviceDetails(spotlight);}); - connect(spotlight, &Spotlight::deviceDeactivated, this, [this, spotlight](){updateDeviceDetails(spotlight);}); + // TODO connect to deviceflag and battery status changes for the current device + // => only update info widget if something changes layout->addWidget(m_deviceDetailsTextEdit); return diWidget; diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 2c9ab651..76469b2b 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -49,7 +49,6 @@ class DevicesWidget : public QWidget // TODO Put into separate DeviceDetailsWidget QTextEdit* m_deviceDetailsTextEdit = nullptr; - QTimer* m_updateDeviceDetailsTimer = nullptr; QPointer m_inputMapper; }; diff --git a/src/hidpp.cc b/src/hidpp.cc index f2e2f870..3b5fdcc9 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -229,7 +229,7 @@ uint8_t Message::address() const { // ------------------------------------------------------------------------------------------------- uint8_t Message::featureIndex() const { - return m_data[Offset::Address]; + return m_data[Offset::FeatureIndex]; } // ------------------------------------------------------------------------------------------------- @@ -699,3 +699,15 @@ const char* toString(HIDPP::BatteryStatus bs) }; return "BatteryStatus::(unknown)"; } + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::Notification n) +{ + using Notification = HIDPP::Notification; + switch (n) + { + ENUM_CASE_STRINGIFY(Notification::DeviceDisconnection); + ENUM_CASE_STRINGIFY(Notification::DeviceConnection); + }; + return "Notification::(unknown)"; +} diff --git a/src/hidpp.h b/src/hidpp.h index 667e80c4..a7a981ca 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -65,6 +65,12 @@ namespace HIDPP { Unsupported = 9, }; + // ----------------------------------------------------------------------------------------------- + enum class Notification : uint8_t { + DeviceDisconnection = 0x40, + DeviceConnection = 0x41, + }; + // ----------------------------------------------------------------------------------------------- namespace Commands { constexpr uint8_t SetRegister = 0x80; @@ -254,6 +260,25 @@ class HidppConnectionInterface using DataBatchResultCallback = std::function&&)>; virtual void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, bool continueOnError = false) = 0; + + // --- + + using NotificationCallback = std::function; + // The registered notification callback will be automatically unregistered if obj is destroyed. + virtual void registerNotificationCallback(QObject* obj, + uint8_t featureIndex, + NotificationCallback cb, + uint8_t function = 0xff) = 0; + virtual void registerNotificationCallback(QObject* obj, + HIDPP::Notification n, + NotificationCallback cb, + uint8_t function = 0xff) = 0; + + + virtual void unregisterNotificationCallback(QObject* obj, uint8_t featureIndex, + uint8_t function = 0xff) = 0; + virtual void unregisterNotificationCallback(QObject* obj, HIDPP::Notification n, + uint8_t function = 0xff) = 0; }; namespace HIDPP { @@ -336,4 +361,5 @@ const char* toString(HidppConnectionInterface::MsgResult r); const char* toString(HIDPP::Error e); const char* toString(HIDPP::FeatureSet::State s); const char* toString(HIDPP::FeatureCode fc); -const char* toString(HIDPP::BatteryStatus bs); \ No newline at end of file +const char* toString(HIDPP::BatteryStatus bs); +const char* toString(HIDPP::Notification n); diff --git a/src/spotlight.cc b/src/spotlight.cc index ef36007b..56f3e415 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -24,6 +24,9 @@ DECLARE_LOGGING_CATEGORY(input) namespace { const auto hexId = logging::hexId; + + // See details on workaround in onEventDataAvailable + bool workaroundLogitechFirstMoveEvent = true; } // --- end anonymous namespace // ------------------------------------------------------------------------------------------------- @@ -39,6 +42,7 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) connect(m_activeTimer, &QTimer::timeout, this, [this](){ setSpotActive(false); + workaroundLogitechFirstMoveEvent = true; }); if (m_options.enableUInput) { @@ -146,7 +150,22 @@ int Spotlight::connectDevices() { if (dc->hasHidppSupport()) { - if (auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc)) { + if (auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc)) + { + // Remove device on socketReadError + QPointer connPtr(hidppCon.get()); + connect(&*hidppCon, &SubHidppConnection::socketReadError, this, [this, connPtr](){ + if (!connPtr) return; + const bool anyConnectedBefore = anySpotlightDeviceConnected(); + connPtr->disconnect(); + QTimer::singleShot(0, this, [this, devicePath=connPtr->path(), anyConnectedBefore](){ + removeDeviceConnection(devicePath); + if (!anySpotlightDeviceConnected() && anyConnectedBefore) { + emit anySpotlightDeviceConnectedChanged(false); + } + }); + }); + return hidppCon; } } @@ -308,11 +327,30 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) const auto &first_ev = buf[0]; const bool isMouseMoveEvent = first_ev.type == EV_REL && (first_ev.code == REL_X || first_ev.code == REL_Y); + if (isMouseMoveEvent) { // Skip input mapping for mouse move events completely - if (!m_activeTimer->isActive()) { + + // Note: During a Next or Back button press the Logitech Spotlight device can send + // move events via hid++ notifications. It seems that just when releasing the + // next or back button sometimes a mouse move event 'leaks' through here as + // relative input event causing the spotlight to be activated. + // The workaround skips a first input move event from the logitech spotlight device. + const bool isLogitechSpotlight = connection.deviceId().vendorId == 0x46d + && (connection.deviceId().productId == 0xc53e || connection.deviceId().productId == 0xb503); + const bool logitechIsFirst = isLogitechSpotlight && workaroundLogitechFirstMoveEvent; + + if (isLogitechSpotlight) + { + workaroundLogitechFirstMoveEvent = false; + if(!logitechIsFirst) { + if (!spotActive()) setSpotActive(true); + } + } + else if (!m_activeTimer->isActive()) { setSpotActive(true); } + m_activeTimer->start(); if (m_virtualDevice) m_virtualDevice->emitEvents(buf.data(), buf.pos()); } @@ -336,71 +374,12 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) // // ------------------------------------------------------------------------------------------------- // void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) // { -// Q_UNUSED(fd); -// Q_UNUSED(connection); -// QByteArray readVal(20, 0); -// if (::read(fd, static_cast(readVal.data()), readVal.length()) < 0) -// { -// if (errno != EAGAIN) -// { -// const bool anyConnectedBefore = anySpotlightDeviceConnected(); -// connection.disconnect(); -// QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ -// removeDeviceConnection(devicePath); -// if (!anySpotlightDeviceConnected() && anyConnectedBefore) { -// emit anySpotlightDeviceConnectedChanged(false); -// } -// }); -// } -// return; -// } - -// // Only process HID++ packets (hence, the packets starting with 0x10 or 0x11) -// if (!(readVal.at(0) == HIDPP::Bytes::SHORT_MSG || readVal.at(0) == HIDPP::Bytes::LONG_MSG)) { -// return; -// } - -// logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - -// if (readVal.at(0) == HIDPP::Bytes::SHORT_MSG) // Logitech HIDPP SHORT message: 7 byte long -// { -// // wireless notification from USB dongle -// if (readVal.at(2) == HIDPP::Bytes::SHORT_WIRELESS_NOTIFICATION_CODE) { -// auto connection_status = readVal.at(4) & (1<<6); // should be zero for working connection between -// // USB dongle and Spotlight device. -// if (connection_status) { // connection between USB dongle and spotlight device broke -// connection.setHIDppProtocol(-1); -// } else { // Logitech spotlight presenter unit got online and USB dongle acknowledged it. -// if (!connection.isOnline()) connection.initialize(); -// } -// } -// } - -// if (readVal.at(0) == HIDPP::Bytes::LONG_MSG) // Logitech HIDPP LONG message: 20 byte long -// { -// // response to ping -// auto rootIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::Root); -// if (readVal.at(2) == rootIndex) { -// if (readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10) && readVal.at(6) == 0x5d) { -// auto protocolVer = static_cast(readVal.at(4)) + static_cast(readVal.at(5))/10.0; -// connection.setHIDppProtocol(protocolVer); -// } -// } - // // Wireless Notification from the Spotlight device // auto wnIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::WirelessDeviceStatus); // if (wnIndex && readVal.at(2) == wnIndex) { // Logitech spotlight presenter unit got online. // if (!connection.isOnline()) connection.initialize(); // } -// // Battery packet processing: Device responded to BatteryStatus (0x1000) packet -// auto batteryIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::BatteryStatus); -// if (batteryIndex && readVal.at(2) == batteryIndex && -// readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x00)) { // Battery information packet -// QByteArray batteryData(readVal.mid(4, 3)); -// emit connection.receivedBatteryInfo(batteryData); -// } - // // Process reprogrammed keys : Next Hold and Back Hold // auto rcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::ReprogramControlsV4); // if (rcIndex && readVal.at(2) == rcIndex) // Button (for which hold events are on) related message. @@ -468,12 +447,6 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) // m_holdButtonStatus.addEvent(); // } // } - -// // Vibration response check -// const uint8_t pcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::PresenterControl); -// if (pcIndex && readVal.at(2) == pcIndex && readVal.at(3) == connection.getFeatureSet()->getRandomFunctionCode(0x10)) { -// logDebug(hid) << "Device acknowledged a vibration event."; -// } // } // } @@ -493,24 +466,6 @@ bool Spotlight::addInputEventHandler(std::shared_ptr connect return true; } -// // ------------------------------------------------------------------------------------------------- -// bool Spotlight::addHidppInputHandler(std::shared_ptr connection) -// { -// if (!connection || connection->type() != ConnectionType::Hidraw -// || !connection->isConnected() || !connection->hasFlags(DeviceFlag::Hidpp)) -// { -// return false; -// } - -// QSocketNotifier* const readNotifier = connection->socketReadNotifier(); -// connect(readNotifier, &QSocketNotifier::activated, this, -// [this, connection=std::move(connection)](int fd) { -// onHidppDataAvailable(fd, *connection.get()); -// }); - -// return true; -// } - // ------------------------------------------------------------------------------------------------- bool Spotlight::setupDevEventInotify() { diff --git a/src/spotlight.h b/src/spotlight.h index edd8176b..9ae12e82 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -75,8 +75,6 @@ class Spotlight : public QObject signals: void deviceConnected(const DeviceId& id, const QString& name); void deviceDisconnected(const DeviceId& id, const QString& name); - void deviceActivated(const DeviceId& id, const QString& name); - void deviceDeactivated(const DeviceId& id, const QString& name); void subDeviceConnected(const DeviceId& id, const QString& name, const QString& path); void subDeviceDisconnected(const DeviceId& id, const QString& name, const QString& path); void anySpotlightDeviceConnectedChanged(bool connected); @@ -87,13 +85,11 @@ class Spotlight : public QObject ConnectionResult connectSpotlightDevice(const QString& devicePath, bool verbose = false); bool addInputEventHandler(std::shared_ptr connection); - // bool addHidppInputHandler(std::shared_ptr connection); bool setupDevEventInotify(); int connectDevices(); void removeDeviceConnection(const QString& devicePath); void onEventDataAvailable(int fd, SubEventConnection& connection); - // void onHidppDataAvailable(int fd, SubHidppConnection& connection); const Options m_options; std::map> m_deviceConnections; From 345b92c33ebf0d119bb073a0926e5fbe3ee0f4ff Mon Sep 17 00:00:00 2001 From: Jahn Date: Mon, 13 Sep 2021 20:06:49 +0200 Subject: [PATCH 066/110] Refactoring of hid++ functionality part 5. --- src/actiondelegate.cc | 11 ++- src/asynchronous.h | 14 +-- src/device-hidpp.cc | 66 +++++++++---- src/device-hidpp.h | 13 ++- src/device.cc | 3 + src/device.h | 5 +- src/deviceinput.cc | 73 +++++++++++++- src/deviceinput.h | 58 +++++------ src/hidpp.h | 15 ++- src/inputmapconfig.cc | 6 +- src/inputseqedit.cc | 27 +++--- src/settings.cc | 10 +- src/spotlight.cc | 217 ++++++++++++++++++++++++------------------ src/spotlight.h | 34 ++----- 14 files changed, 343 insertions(+), 209 deletions(-) diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index 212251a8..87772eac 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -349,11 +349,12 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* const auto& item = model->configData(index); if (!item.action) return; - const bool showRepeatedActions = std::any_of( - ReservedKeyEventSequence::HoldButtonsInfo.cbegin(), - ReservedKeyEventSequence::HoldButtonsInfo.cend(), - [item](const auto& button){ - return (item.deviceSequence == button.keqEventSeq);}); + const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); + const bool showRepeatedActions = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), + [&item](const auto& specialKeyInfo){ + return (item.deviceSequence == specialKeyInfo.second.keyEventSeq); + } + ); struct actionEntry { Action::Type type; diff --git a/src/asynchronous.h b/src/asynchronous.h index d310dd41..aded391e 100644 --- a/src/asynchronous.h +++ b/src/asynchronous.h @@ -96,10 +96,10 @@ struct remove_member { /// Create a safe function object, guaranteed to be invoked in the context of /// the given QObject context. template -auto makeSafeCallback_impl(QObject* context, F&& f, std::function, bool forceQueued) +auto makeSafeCallback_impl(QObject* context, F&& f, std::function, bool autoConnection) { QPointer ctxPtr(context); - return [ctxPtr, forceQueued, f=std::forward(f)](Args&&... args) mutable + return [ctxPtr, autoConnection, f=std::forward(f)](Args&&... args) mutable { // Check if context object is still valid if (ctxPtr.isNull()) { @@ -109,7 +109,7 @@ auto makeSafeCallback_impl(QObject* context, F&& f, std::function, b #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QMetaObject::invokeMethod(ctxPtr, capture_call(std::forward(f), std::forward(args)...), - forceQueued ? Qt::QueuedConnection : Qt::AutoConnection); + autoConnection ? Qt::AutoConnection : Qt::QueuedConnection); // Note: if forceQueued is false and current thread is the same as // the context thread -> execute directly #else @@ -122,10 +122,10 @@ auto makeSafeCallback_impl(QObject* context, F&& f, std::function, b /// Create a safe function object, guaranteed to be invoked in the context of /// the given QObject context. template -auto makeSafeCallback(QObject* context, F&& f, bool forceQueued = true) { +auto makeSafeCallback(QObject* context, F&& f, bool autoConnection) { using sig = decltype(&F::operator()); using ft = std::function::type>; - return async::makeSafeCallback_impl(context, std::forward(f), ft{}, forceQueued); + return async::makeSafeCallback_impl(context, std::forward(f), ft{}, autoConnection); } /// Deriving from this class will makeSafeCallback and postSelf methods for QObject based @@ -144,8 +144,8 @@ class Async protected: /// Returns a function object that is guaranteed to be invoked in the own thread context. template - auto makeSafeCallback(F&& f, bool forceQueued = true) { - return async::makeSafeCallback(static_cast(this), std::forward(f), forceQueued); + auto makeSafeCallback(F&& f, bool autoConnection = true) { + return async::makeSafeCallback(static_cast(this), std::forward(f), autoConnection); } /// Post a function to the own event loop. diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index 74fbbe71..4cea645a 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -176,7 +176,7 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r if (it->callBack) { it->callBack(result, HIDPP::Message()); } m_requests.erase(it); - }, false)); + })); // Place request in request list with a timeout m_requests.emplace_back(RequestEntry{ @@ -232,7 +232,7 @@ void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallb // continue processing the rest of the batch sendDataBatch(std::move(batch), std::move(batchCb), coe, std::move(results)); - }, false)); + })); }); } @@ -280,7 +280,7 @@ void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatc // continue processing the rest of the batch sendRequestBatch(std::move(batch), std::move(batchCb), coe, std::move(results)); - }, false)); + }, true)); }); } @@ -460,6 +460,15 @@ void SubHidppConnection::setPresenterState(PresenterState ps) emit presenterStateChanged(m_presenterState); } +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::setBatteryInfo(const HIDPP::BatteryInfo& bi) +{ + if (m_batteryInfo == bi) return; + + m_batteryInfo = bi; + emit batteryInfoChanged(m_batteryInfo); +} + // ------------------------------------------------------------------------------------------------- void SubHidppConnection::initReceiver(std::function cb) { @@ -594,14 +603,15 @@ void SubHidppConnection::initPresenter(std::function cb) logDebug(hid) << tr("InitFeature result %1 => %2").arg(toString(res.first)).arg(toString(res.second)); } } + emit featureSetInitialized(); setPresenterState(PresenterState::Initialized_Online); if (cb) cb(m_presenterState); - }, false)); + })); return; } } if (cb) cb(m_presenterState); - }, false)); + })); }); } @@ -694,12 +704,12 @@ void SubHidppConnection::updateDeviceFlags() } if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::ReprogramControlsV4)) { - auto& reservedInputs = m_inputMapper->getReservedInputs(); + auto& reservedInputs = m_inputMapper->reservedInputs(); reservedInputs.clear(); featureFlagsSet |= DeviceFlags::NextHold; featureFlagsSet |= DeviceFlags::BackHold; - reservedInputs.emplace_back(ReservedKeyEventSequence::NextHoldInfo); - reservedInputs.emplace_back(ReservedKeyEventSequence::BackHoldInfo); + reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold)); + reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold)); logDebug(hid) << tr("Subdevice '%1' reported %2 support.") .arg(path()).arg(toString(HIDPP::FeatureCode::ReprogramControlsV4)); } @@ -745,7 +755,7 @@ void SubHidppConnection::registerForFeatureNotifications() logDebug(hid) << tr("Buttons pressed: Next = %1, Back = %2") .arg(isNextPressed).arg(isBackPressed) << msg.hex(); - }, false), 0 /* function 0 */); + }), 0 /* function 0 */); registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) { @@ -763,14 +773,15 @@ void SubHidppConnection::registerForFeatureNotifications() // .arg(cast(msg[5]), 4) // .arg(cast(msg[6]), 4) // .arg(cast(msg[7]), 4); - }, false), 1 /* function 1 */); + }), 1 /* function 1 */); } if (const auto batIndex = m_featureSet.featureIndex(FeatureCode::BatteryStatus)) { - // TODO register for BatteryLevelStatusBroadcastEvent // A device can send a battery status spontaneously to the software. - } + registerNotificationCallback(this, batIndex, makeSafeCallback([this](Message&& msg) { + setBatteryInfo(BatteryInfo{msg[4], msg[5], to_enum(msg[6])}); + }), 0 /* function 0 */); } } // ------------------------------------------------------------------------------------------------- @@ -797,9 +808,9 @@ void SubHidppConnection::registerForUsbNotifications() logInfo(hid) << tr("Device '%1' came online.").arg(path()); checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState /* ps */) { //... - }, false)); + })); } - }, false)); + })); } // ------------------------------------------------------------------------------------------------- @@ -817,8 +828,8 @@ void SubHidppConnection::subDeviceInit() // presenter device HID++ features and more checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState /* ps */) { //... - }, false)); - }, false)); + })); + })); } // ------------------------------------------------------------------------------------------------- @@ -836,6 +847,25 @@ HIDPP::ProtocolVersion SubHidppConnection::protocolVersion() const { return m_protocolVersion; } +// ------------------------------------------------------------------------------------------------- +void SubHidppConnection::triggerBattyerInfoUpdate() +{ + using namespace HIDPP; + getBatteryLevelStatus(makeSafeCallback([this](MsgResult res, BatteryInfo&& bi) + { + if (res != MsgResult::Ok) { + return; + } + + setBatteryInfo(bi); + })); +} + +// ------------------------------------------------------------------------------------------------- +const HIDPP::BatteryInfo& SubHidppConnection::batteryInfo() const { + return m_batteryInfo; +} + // ------------------------------------------------------------------------------------------------- void SubHidppConnection::sendPing(RequestResultCallback cb) { @@ -940,14 +970,14 @@ void SubHidppConnection::checkAndUpdatePresenterState(std::function cb); + void sendPing(RequestResultCallback cb); void sendVibrateCommand(uint8_t intensity, uint8_t length, RequestResultCallback cb); /// Set device pointer speed - speed needs to be in the range [0-9] void setPointerSpeed(uint8_t speed, RequestResultCallback cb); @@ -77,6 +79,9 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt signals: void receiverStateChanged(ReceiverState); void presenterStateChanged(PresenterState); + void featureSetInitialized(); + + void batteryInfoChanged(const HIDPP::BatteryInfo&); private: void subDeviceInit(); @@ -88,8 +93,11 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt /// Initializes features. Returns a map of initalized features and the result from it. void initFeatures(std::function&&)> cb); + void getBatteryLevelStatus(std::function cb); + void setReceiverState(ReceiverState rs); void setPresenterState(PresenterState ps); + void setBatteryInfo(const HIDPP::BatteryInfo& bi); void onHidppDataAvailable(int fd); @@ -106,6 +114,7 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt HIDPP::FeatureSet m_featureSet; HIDPP::ProtocolVersion m_protocolVersion; + HIDPP::BatteryInfo m_batteryInfo; ReceiverState m_receiverState = ReceiverState::Uninitialized; PresenterState m_presenterState = PresenterState::Uninitialized; diff --git a/src/device.cc b/src/device.cc index 36586ae5..946c7335 100644 --- a/src/device.cc +++ b/src/device.cc @@ -173,6 +173,9 @@ QSocketNotifier* SubDeviceConnection::socketReadNotifier() { SubEventConnection::SubEventConnection(Token, const DeviceId& dId, const DeviceScan::SubDevice& sd) : SubDeviceConnection(dId, sd, ConnectionType::Event, ConnectionMode::ReadOnly) {} +// ------------------------------------------------------------------------------------------------- +SubEventConnection::~SubEventConnection() = default; + // ------------------------------------------------------------------------------------------------- bool SubEventConnection::isConnected() const { return (m_readNotifier && m_readNotifier->isEnabled()); diff --git a/src/device.h b/src/device.h index ac5d9e03..cc2e0a4f 100644 --- a/src/device.h +++ b/src/device.h @@ -42,8 +42,6 @@ class DeviceConnection : public QObject bool removeSubDevice(const QString& path); const auto& subDevices() { return m_subDeviceConnections; } - // // TODO ... Refactor battery status handling - signals: void subDeviceConnected(const DeviceId& id, const QString& path); void subDeviceDisconnected(const DeviceId& id, const QString& path); @@ -57,8 +55,6 @@ class DeviceConnection : public QObject QString m_deviceName; std::shared_ptr m_inputMapper; ConnectionMap m_subDeviceConnections; - - // BatteryInfo m_batteryInfo; // TODO.. }; // ------------------------------------------------------------------------------------------------- @@ -154,6 +150,7 @@ class SubEventConnection : public SubDeviceConnection const DeviceConnection& dc); SubEventConnection(Token, const DeviceId&, const DeviceScan::SubDevice&); + virtual ~SubEventConnection(); bool isConnected() const; auto& inputBuffer() { return m_inputEventBuffer; } diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 810c62f9..22522899 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -3,6 +3,7 @@ #include "deviceinput.h" +#include "enum-helper.h" #include "logging.h" #include "settings.h" #include "virtualdevice.h" @@ -39,6 +40,20 @@ namespace { } return QKeySequence(); } + + // ----------------------------------------------------------------------------------------------- + KeyEventSequence makeSpecialKeyEventSequence(uint16_t code) + { + // Special key event with 3 button presses of the same key, + // which should not be able with real events + KeyEvent pressed { + {EV_KEY, code, 1}, + {EV_KEY, code, 1}, + {EV_KEY, code, 1}, + }; + + return KeyEventSequence{std::move(pressed)}; + }; } // ------------------------------------------------------------------------------------------------- @@ -93,6 +108,27 @@ QDebug operator<<(QDebug debug, const KeyEvent &ke) return debug; } +// ------------------------------------------------------------------------------------------------- +std::shared_ptr GlobalActions::scrollHorizontal() +{ + static auto scrollHorizontalAction = std::make_shared(); + return scrollHorizontalAction; +} + +// ------------------------------------------------------------------------------------------------- +std::shared_ptr GlobalActions::scrollVertical() +{ + static auto scrollVerticalAction = std::make_shared(); + return scrollVerticalAction; +} + +// ------------------------------------------------------------------------------------------------- +std::shared_ptr GlobalActions::volumeControl() +{ + static auto volumeControlAction = std::make_shared(); + return volumeControlAction; +} + // ------------------------------------------------------------------------------------------------- QDataStream& operator>>(QDataStream& s, MappedAction& mia) { std::underlying_type_t type; @@ -263,7 +299,7 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) previous->nextMap.push_back(current); } - // if last item in key event set + // if last item in key event sequence if (i == kes.size() - 1) { current->action = configItem.second.action; } @@ -495,6 +531,8 @@ struct InputMapper::Impl std::vector m_events; InputMapConfig m_config; bool m_recordingMode = false; + + ReservedInputs m_reservedInputs; }; // ------------------------------------------------------------------------------------------------- @@ -790,3 +828,36 @@ const InputMapConfig& InputMapper::configuration() const { return impl->m_config; } + +// ------------------------------------------------------------------------------------------------- +InputMapper::ReservedInputs& InputMapper::reservedInputs() +{ + return impl->m_reservedInputs; +} + +// ------------------------------------------------------------------------------------------------- +namespace SpecialKeys +{ +// ------------------------------------------------------------------------------------------------- +const std::map& keyEventSequenceMap() +{ + static const std::map keyMap { + {Key::BackHold, {"Back Hold", makeSpecialKeyEventSequence(to_integral(Key::BackHold))}}, + {Key::NextHold, {"Next Hold", makeSpecialKeyEventSequence(to_integral(Key::NextHold))}}, + }; + return keyMap; +} + +// ------------------------------------------------------------------------------------------------- +const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key) +{ + const auto it = keyEventSequenceMap().find(key); + if (it != keyEventSequenceMap().cend()) { + return it->second; + } + + static const SpecialKeyEventSeqInfo notFound; + return notFound; +} + +} // end namespace SpecialKeys \ No newline at end of file diff --git a/src/deviceinput.h b/src/deviceinput.h index da43811c..b6d14c02 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -2,8 +2,6 @@ // - See LICENSE.md and README.md #pragma once -#include - #include #include @@ -78,39 +76,25 @@ QDebug operator<<(QDebug debug, const DeviceInputEvent &ie); QDebug operator<<(QDebug debug, const KeyEvent &ke); // ------------------------------------------------------------------------------------------------- -// Some inputs from Logitech Spotlight device (like Next Hold and Back Hold events) are not valid -// input event (input_event in linux/input.h) in conventional sense. Rather they are communicated +// Some inputs from Logitech Spotlight device (like Next Hold and Back Hold events) are not a valid +// input event (input_event in linux/input.h) in a conventional sense. They are communicated // via HID++ messages from the device. To use to InputMapper architechture in that case we need to // reserve some KeyEventSequence for such events. These KeyEventSequence should be designed in -// such a way that they cannot interfare with other valid input events from the device. -namespace ReservedKeyEventSequence { - const auto genKeyEventsWithoutSYN = [](std::vector codes){ - KeyEventSequence ks; - for (auto code: codes) - { - KeyEvent pressed; KeyEvent released; - pressed.emplace_back(EV_KEY, code, 1); - released.emplace_back(EV_KEY, code, 0); - ks.emplace_back(std::move(pressed)); - ks.emplace_back(std::move(released)); - } - return ks; +// such a way that they cannot interfere with other valid input events from the device. +namespace SpecialKeys +{ + enum class Key : uint16_t { + NextHold = 0x0ff0, + BackHold = 0x0ff1, }; - struct ReservedKeyEventSeqInfo {QString name; - KeyEventSequence keqEventSeq = {}; - }; - - // Use four key codes for Next and Back Hold event - const ReservedKeyEventSeqInfo NextHoldInfo = {"Next Hold", - genKeyEventsWithoutSYN({KEY_H, KEY_N, KEY_X, KEY_T})}; //HNXT: Reserved for Next Hold event - const ReservedKeyEventSeqInfo BackHoldInfo = {"Back Hold", - genKeyEventsWithoutSYN({KEY_H, KEY_B, KEY_C, KEY_K})}; //HBCK: Reserved for Back Hold event - - const std::array HoldButtonsInfo {{NextHoldInfo, BackHoldInfo}}; + struct SpecialKeyEventSeqInfo { + QString name; + KeyEventSequence keyEventSeq; + }; - // Currently HoldButtonsInfo are the only reserved keys. May change in the future. - const auto ReservedKeyEvents = HoldButtonsInfo; + const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key); + const std::map& keyEventSequenceMap(); } // ------------------------------------------------------------------------------------------------- @@ -278,6 +262,13 @@ struct VolumeControlAction : public Action int param = 0; }; +// ------------------------------------------------------------------------------------------------- +namespace GlobalActions { + std::shared_ptr scrollHorizontal(); + std::shared_ptr scrollVertical(); + std::shared_ptr volumeControl(); +} + // ------------------------------------------------------------------------------------------------- struct MappedAction { @@ -292,9 +283,6 @@ QDataStream& operator<<(QDataStream& s, const MappedAction& mia); // ------------------------------------------------------------------------------------------------- class InputMapConfig : public std::map{}; -// ------------------------------------------------------------------------------------------------- -using ReservedInput = std::vector; - // ------------------------------------------------------------------------------------------------- class InputMapper : public QObject { @@ -316,7 +304,8 @@ class InputMapper : public QObject int keyEventInterval() const; void setKeyEventInterval(int interval); - auto& getReservedInputs() { return reservedInputs; } + using ReservedInputs = std::vector; + ReservedInputs& reservedInputs(); std::shared_ptr virtualDevice() const; bool hasVirtualDevice() const; @@ -339,6 +328,5 @@ class InputMapper : public QObject private: struct Impl; std::unique_ptr impl; - ReservedInput reservedInputs; QTimer* m_repeatedActionTimer = nullptr; // Timer for introducing time gap between repeated ations }; diff --git a/src/hidpp.h b/src/hidpp.h index a7a981ca..2686ca15 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -92,11 +93,17 @@ namespace HIDPP { ChargingError = 0x07 }; + // ------------------------------------------------------------------------------------------------- struct BatteryInfo { uint8_t currentLevel = 0; uint8_t nextReportedLevel = 0; BatteryStatus status = BatteryStatus::Discharging; + + inline bool operator==(const BatteryInfo& rhs) const { + return std::tie(currentLevel, nextReportedLevel, status) + == std::tie(rhs.currentLevel, rhs.nextReportedLevel, rhs.status); + } }; // ----------------------------------------------------------------------------------------------- @@ -104,13 +111,17 @@ namespace HIDPP { uint8_t major = 0; uint8_t minor = 0; - bool smallerThan(uint8_t otherMajor, uint8_t otherMinor) const { + inline bool smallerThan(uint8_t otherMajor, uint8_t otherMinor) const { return (major < otherMajor) ? true : (minor < otherMinor) ? true : false; } - bool operator<(const ProtocolVersion& other) const { + inline bool operator<(const ProtocolVersion& other) const { return smallerThan(other.major, other.minor); } + + inline bool operator==(const ProtocolVersion& rhs) const { + return std::tie(major, minor) == std::tie(rhs.major, rhs.minor); + } }; // ----------------------------------------------------------------------------------------------- diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index 7c09240d..49f4e300 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -207,13 +207,13 @@ void InputMapConfigModel::setItemActionType(const QModelIndex& idx, Action::Type item.action = std::make_shared(); break; case Action::Type::ScrollHorizontal: - item.action = std::make_shared(); + item.action = GlobalActions::scrollHorizontal(); break; case Action::Type::ScrollVertical: - item.action = std::make_shared(); + item.action = GlobalActions::scrollVertical(); break; case Action::Type::VolumeControl: - item.action = std::make_shared(); + item.action = GlobalActions::volumeControl(); break; } diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 52b102f4..9e485bbb 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -8,6 +8,7 @@ #include "logging.h" #include +#include #include #include #include @@ -400,23 +401,24 @@ void InputSeqDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti // Our custom drawing of the KeyEventSequence... const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; - const auto keySeq = imModel->configData(index).deviceSequence; - const auto selHoldButton = [keySeq](){ - using namespace ReservedKeyEventSequence; - for (auto& button : HoldButtonsInfo) { - if (keySeq == button.keqEventSeq) return button; + const auto& keySeq = imModel->configData(index).deviceSequence; + const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); + + const auto it = std::find_if(specialKeysMap.cbegin(), specialKeysMap.cend(), + [&keySeq](const auto& specialKeyInfo){ + return (keySeq == specialKeyInfo.second.keyEventSeq); } - return ReservedKeyEventSeqInfo(); - }(); + ); - if (!selHoldButton.keqEventSeq.empty()) + if (it != specialKeysMap.cend()) { - drawPlaceHolderText(xPos, *painter, option, selHoldButton.name, false); + drawPlaceHolderText(xPos, *painter, option, it->second.name, false); } else { - drawKeyEventSequence(xPos, *painter, option, imModel->configData(index).deviceSequence); + drawKeyEventSequence(xPos, *painter, option, keySeq); } + if (option.state & QStyle::State_HasFocus) { drawCurrentIndicator(*painter, option); } @@ -511,14 +513,13 @@ QSize InputSeqDelegate::sizeHint(const QStyleOptionViewItem& option, return QStyledItemDelegate::sizeHint(option, index); } -#include // ------------------------------------------------------------------------------------------------- void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos) { if (!index.isValid() || !model) return; - auto reservedInputs = model->inputMapper()->getReservedInputs(); + const auto& reservedInputs = model->inputMapper()->reservedInputs(); if (!reservedInputs.empty()) { QMenu* menu = new QMenu(parent); @@ -526,7 +527,7 @@ void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* for (const auto& button : reservedInputs) { const auto qaction = menu->addAction(button.name); connect(qaction, &QAction::triggered, this, [model, index, button](){ - model->setInputSequence(index, button.keqEventSeq); + model->setInputSequence(index, button.keyEventSeq); }); } diff --git a/src/settings.cc b/src/settings.cc index dec74341..c18fa3bf 100644 --- a/src/settings.cc +++ b/src/settings.cc @@ -844,7 +844,15 @@ InputMapConfig Settings::getDeviceInputMapConfig(const DeviceId& dId) if (!seq.canConvert()) continue; const auto conf = m_settings->value("mappedAction"); if (!conf.canConvert()) continue; - cfg.emplace(qvariant_cast(seq), qvariant_cast(conf)); + auto mappedAction = qvariant_cast(conf); + if (mappedAction.action->type() == Action::Type::ScrollHorizontal) { + mappedAction.action = GlobalActions::scrollHorizontal(); + } else if (mappedAction.action->type() == Action::Type::ScrollVertical) { + mappedAction.action = GlobalActions::scrollVertical(); + } else if (mappedAction.action->type() == Action::Type::VolumeControl) { + mappedAction.action = GlobalActions::volumeControl(); + } + cfg.emplace(qvariant_cast(seq), std::move(mappedAction)); } m_settings->endArray(); diff --git a/src/spotlight.cc b/src/spotlight.cc index 56f3e415..e55ecc01 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -3,7 +3,7 @@ #include "spotlight.h" -#include "device.h" +#include "deviceinput.h" #include "device-hidpp.h" #include "logging.h" #include "settings.h" @@ -27,8 +27,36 @@ namespace { // See details on workaround in onEventDataAvailable bool workaroundLogitechFirstMoveEvent = true; + } // --- end anonymous namespace + +// ------------------------------------------------------------------------------------------------- +struct HoldButtonStatus { + enum class HoldButtonType : uint8_t { None, Next, Back }; + + void setButton(HoldButtonType b){ _button = b; _numEvents=0; }; + auto getButton() const { return _button; } + int numEvents() const { return _numEvents; }; + void addEvent(){ _numEvents++; }; + void reset(){ setButton(HoldButtonType::None); }; + auto keyEventSeq() { + switch (_button){ + case HoldButtonType::Next: + return SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold).keyEventSeq; + case HoldButtonType::Back: + return SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold).keyEventSeq; + case HoldButtonType::None: + return KeyEventSequence(); + } + return KeyEventSequence(); + }; + +private: + HoldButtonType _button = HoldButtonType::None; + unsigned long _numEvents = 0; +}; + // ------------------------------------------------------------------------------------------------- Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) : QObject(parent) @@ -36,6 +64,7 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) , m_activeTimer(new QTimer(this)) , m_connectionTimer(new QTimer(this)) , m_settings(settings) + , m_holdButtonStatus(std::make_unique()) { m_activeTimer->setSingleShot(true); m_activeTimer->setInterval(600); @@ -53,7 +82,8 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) } m_connectionTimer->setSingleShot(true); - // From detecting a change from inotify, the device needs some time to be ready for open + // From detecting a change with inotify, the device needs some time to be ready for open, + // otherwise opening the device will fail. // TODO: This interval seems to work, but it is arbitrary - there should be a better way. m_connectionTimer->setInterval(800); @@ -152,8 +182,15 @@ int Spotlight::connectDevices() { if (auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc)) { - // Remove device on socketReadError QPointer connPtr(hidppCon.get()); + + connect(&*hidppCon, &SubHidppConnection::featureSetInitialized, this, + [this, connPtr](){ + if (!connPtr) { return; } + this->registerForNotifications(connPtr.data()); + }); + + // Remove device on socketReadError connect(&*hidppCon, &SubHidppConnection::socketReadError, this, [this, connPtr](){ if (!connPtr) return; const bool anyConnectedBefore = anySpotlightDeviceConnected(); @@ -193,7 +230,7 @@ int Spotlight::connectDevices() connect(im, &InputMapper::actionMapped, this, [this](std::shared_ptr action) { - if (!(action->isRepeated()) && m_holdButtonStatus.numEvents() > 0) return; + if (!(action->isRepeated()) && m_holdButtonStatus->numEvents() > 0) return; if (action->type() == Action::Type::CyclePresets) { @@ -216,14 +253,16 @@ int Spotlight::connectDevices() { if (!m_virtualDevice) return; - int param = 0; - if (action->type() == Action::Type::ScrollHorizontal) param = static_cast(action.get())->param; - if (action->type() == Action::Type::ScrollVertical) param = static_cast(action.get())->param; - - uint16_t wheelCode = (action->type() == Action::Type::ScrollHorizontal) ? REL_HWHEEL : REL_WHEEL; - const std::vector scrollInputEvents = {{{}, EV_REL, wheelCode, param}, {{}, EV_SYN, SYN_REPORT, 0},}; + const int param = (action->type() == Action::Type::ScrollHorizontal) + ? static_cast(action.get())->param + : static_cast(action.get())->param; - if (param) m_virtualDevice->emitEvents(scrollInputEvents); + if (param) + { + const uint16_t wheelCode = (action->type() == Action::Type::ScrollHorizontal) ? REL_HWHEEL : REL_WHEEL; + const std::vector scrollInputEvents = {{{}, EV_REL, wheelCode, param}, {{}, EV_SYN, SYN_REPORT, 0},}; + m_virtualDevice->emitEvents(scrollInputEvents); + } } else if (action->type() == Action::Type::VolumeControl) { @@ -231,7 +270,7 @@ int Spotlight::connectDevices() auto param = static_cast(action.get())->param; uint16_t keyCode = (param > 0)? KEY_VOLUMEUP: KEY_VOLUMEDOWN; - const std::vector curVolInputEvents = {{{}, EV_KEY, keyCode, abs(param)}, {{}, EV_SYN, SYN_REPORT, 0}, + const std::vector curVolInputEvents = {{{}, EV_KEY, keyCode, 1}, {{}, EV_SYN, SYN_REPORT, 0}, {{}, EV_KEY, keyCode, 0}, {{}, EV_SYN, SYN_REPORT, 0},}; if (param) m_virtualDevice->emitEvents(curVolInputEvents); } @@ -371,84 +410,82 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) } // end while loop } -// // ------------------------------------------------------------------------------------------------- -// void Spotlight::onHidppDataAvailable(int fd, SubHidppConnection& connection) -// { -// // Wireless Notification from the Spotlight device -// auto wnIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::WirelessDeviceStatus); -// if (wnIndex && readVal.at(2) == wnIndex) { // Logitech spotlight presenter unit got online. -// if (!connection.isOnline()) connection.initialize(); -// } - -// // Process reprogrammed keys : Next Hold and Back Hold -// auto rcIndex = connection.getFeatureSet()->getFeatureIndex(FeatureCode::ReprogramControlsV4); -// if (rcIndex && readVal.at(2) == rcIndex) // Button (for which hold events are on) related message. -// { -// auto eventCode = static_cast(readVal.at(3)); -// auto buttonCode = static_cast(readVal.at(5)); -// if (eventCode == 0x00) { // hold start/stop events -// switch (buttonCode) { -// case 0xda: -// logDebug(hid) << "Next Hold Event "; -// m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Next); -// break; -// case 0xdc: -// logDebug(hid) << "Back Hold Event "; -// m_holdButtonStatus.setButton(HoldButtonStatus::HoldButtonType::Back); -// break; -// case 0x00: -// // hold event over. -// logDebug(hid) << "Hold Event over."; -// m_holdButtonStatus.reset(); -// } -// } -// else if (eventCode == 0x10) { // mouse move event -// // Mouse data is sent as 4 byte information starting at 5th byte and ending at 8th. -// // out of these 6th byte and 8th bytes are x and y relative change, respectively. -// // Not sure about meaning of 5th and 7th bytes. However during testing -// // the 5th byte shows horizonal scroll towards right if rel value is -1 otherwise left scroll (0) -// // the 7th byte shows vertical scroll towards up if rel value is -1 otherwise down scroll (0) -// auto byteToRel = [](int i){return ( (i<128) ? i : 256-i);}; // convert the byte to relative motion in x or y -// int x = byteToRel(readVal.at(5)); -// int y = byteToRel(readVal.at(7)); - -// //auto action = connection.inputMapper()->getAction(m_holdButtonStatus.keyEventSeq()); -// auto action = std::shared_ptr{}; - -// if (action && !action->empty()) -// { -// auto getReducedParam = [](int param, int limit=2){ // reduce the values from Spotlight device for better scroll behavior -// int minVal=5; -// if (abs(param) < minVal) return 0; // ignore small device movement - -// auto sign = (param == 0)? 0: ((param > 0)? 1:-1); -// return ((abs(param) > minVal*limit)? sign*minVal*limit : param)/minVal; // limit return value between -limit to limit -// }; - -// if (action->type() == Action::Type::ScrollHorizontal) -// { -// const auto scrollHAction = static_cast(action.get()); -// scrollHAction->param = -(getReducedParam(x)); -// } -// if (action->type() == Action::Type::ScrollVertical) -// { -// const auto scrollVAction = static_cast(action.get()); -// scrollVAction->param = getReducedParam(y); -// } -// if(action->type() == Action::Type::VolumeControl) -// { -// const auto volumeControlAction = static_cast(action.get()); -// volumeControlAction->param = -getReducedParam(y, 3); -// } - -// // feed the keystroke to InputMapper and let it trigger the associated action -// for (auto key_event: m_holdButtonStatus.keyEventSeq()) connection.inputMapper()->addEvents(key_event); -// } -// m_holdButtonStatus.addEvent(); -// } -// } -// } -// } +// ------------------------------------------------------------------------------------------------- +void Spotlight::registerForNotifications(SubHidppConnection* connection) +{ + using namespace HIDPP; + + // Logitech button next and back press and hold + movement + if (const auto rcIndex = connection->featureSet().featureIndex(FeatureCode::ReprogramControlsV4)) + { + connection->registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) + { + // Logitech Spotlight: + // * Next Button = 0xda + // * Back Button = 0xdc + // Byte 5 and 7 indicate pressed buttons + // Back and next can be pressed at the same time + + constexpr uint8_t ButtonNext = 0xda; + constexpr uint8_t ButtonBack = 0xdc; + const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; + const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; + + if (isNextPressed) { + m_holdButtonStatus->setButton(HoldButtonStatus::HoldButtonType::Next); + } else if (isBackPressed) { + m_holdButtonStatus->setButton(HoldButtonStatus::HoldButtonType::Back); + } else { + m_holdButtonStatus->reset(); + } + }), 0 /* function 0 */); + + + connection->registerNotificationCallback(this, rcIndex, + makeSafeCallback([this, connection](Message&& msg) + { + // byte 4 : -1 for left movement, 0 for right movement + // byte 5 : horizontal movement speed -128 to 127 + // byte 6 : -1 for up movement, 0 for down movement + // byte 7 : vertical movement speed -128 to 127 + + static const auto intcast = [](uint8_t v) -> int{ return static_cast(v); }; + // logDebug(hid) << tr("4 = %1, 5 = %2, 6 = %3, 7 = %4") + // .arg(intcast(msg[4]), 4) + // .arg(intcast(msg[5]), 4) + // .arg(intcast(msg[6]), 4) + // .arg(intcast(msg[7]), 4); + + const int x = intcast(msg[5]); + const int y = intcast(msg[7]); + + static const auto getReducedParam = [](int param, int limit=2){ // reduce the values from Spotlight device for better scroll behavior + constexpr int minVal = 5; + if (abs(param) < minVal) return 0; // ignore small device movement + + const auto sign = (param == 0) ? 0 : ((param > 0) ? 1 :-1); + return ((abs(param) > minVal*limit)? sign*minVal*limit : param)/minVal; // limit return value between -limit to limit + }; + + static const auto scrollHAction = GlobalActions::scrollHorizontal(); + scrollHAction->param = -(getReducedParam(x)); + + static const auto scrollVAction = GlobalActions::scrollVertical(); + scrollVAction->param = getReducedParam(y); + + static const auto volumeControlAction = GlobalActions::volumeControl(); + volumeControlAction->param = -getReducedParam(y, 3); + + // feed the keystroke to InputMapper and let it trigger the associated action + for (auto key_event: m_holdButtonStatus->keyEventSeq()) { + connection->inputMapper()->addEvents(key_event); + } + + m_holdButtonStatus->addEvent(); + + }), 1 /* function 1 */); + } +} // ------------------------------------------------------------------------------------------------- bool Spotlight::addInputEventHandler(std::shared_ptr connection) diff --git a/src/spotlight.h b/src/spotlight.h index 9ae12e82..626d2023 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -8,45 +8,22 @@ #include #include +#include "asynchronous.h" #include "devicescan.h" -#include "deviceinput.h" class QTimer; class Settings; class VirtualDevice; class DeviceConnection; class SubEventConnection; +class SubHidppConnection; -// ----------------------------------------------------------------------------------------------- -struct HoldButtonStatus { - enum class HoldButtonType : uint8_t { None, Next, Back }; - - void setButton(HoldButtonType b){ _button = b; _numEvents=0; }; - auto getButton() const { return _button; } - int numEvents() const { return _numEvents; }; - void addEvent(){ _numEvents++; }; - void reset(){ setButton(HoldButtonType::None); }; - auto keyEventSeq() { - using namespace ReservedKeyEventSequence; - switch (_button){ - case HoldButtonType::Next: - return NextHoldInfo.keqEventSeq; - case HoldButtonType::Back: - return BackHoldInfo.keqEventSeq; - case HoldButtonType::None: - return KeyEventSequence(); - } - return KeyEventSequence(); - }; +struct HoldButtonStatus; -private: - HoldButtonType _button = HoldButtonType::None; - unsigned long _numEvents = 0; -}; /// Class handling spotlight device connections and indicating if a device is sending /// sending mouse move events. -class Spotlight : public QObject +class Spotlight : public QObject, public async::Async { Q_OBJECT @@ -85,6 +62,7 @@ class Spotlight : public QObject ConnectionResult connectSpotlightDevice(const QString& devicePath, bool verbose = false); bool addInputEventHandler(std::shared_ptr connection); + void registerForNotifications(SubHidppConnection* connection); bool setupDevEventInotify(); int connectDevices(); @@ -100,5 +78,5 @@ class Spotlight : public QObject bool m_spotActive = false; std::shared_ptr m_virtualDevice; Settings* m_settings = nullptr; - HoldButtonStatus m_holdButtonStatus; + std::unique_ptr m_holdButtonStatus; }; From cbd343d06eeb49922e8d03b9070dac60d2609143 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 14 Sep 2021 06:34:54 +0200 Subject: [PATCH 067/110] Allow hold button press as recorded input event. --- src/device-hidpp.cc | 4 +- src/deviceinput.cc | 8 ++-- src/deviceinput.h | 11 ++++-- src/spotlight.cc | 94 ++++++++++++++++++++++++++++----------------- src/spotlight.h | 1 - 5 files changed, 73 insertions(+), 45 deletions(-) diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index 4cea645a..a671ec87 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -708,8 +708,8 @@ void SubHidppConnection::updateDeviceFlags() reservedInputs.clear(); featureFlagsSet |= DeviceFlags::NextHold; featureFlagsSet |= DeviceFlags::BackHold; - reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold)); - reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold)); + reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove)); + reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove)); logDebug(hid) << tr("Subdevice '%1' reported %2 support.") .arg(path()).arg(toString(HIDPP::FeatureCode::ReprogramControlsV4)); } diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 22522899..8e363db8 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -550,6 +550,7 @@ InputMapper::Impl::Impl(InputMapper* parent, std::shared_ptr vdev void InputMapper::Impl::execAction(const std::shared_ptr& action, DeviceKeyMap::Result r) { if (!action || action->empty()) return; + if (action->isRepeated()) { if(m_parent->m_repeatedActionTimer->isActive()) return; @@ -557,7 +558,7 @@ void InputMapper::Impl::execAction(const std::shared_ptr& action, Device } logDebug(input) << "Input map action, type = " << int(action->type()) - << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit); + << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit) << action->isRepeated(); if (action->type() == Action::Type::KeySequence) { @@ -841,9 +842,10 @@ namespace SpecialKeys // ------------------------------------------------------------------------------------------------- const std::map& keyEventSequenceMap() { + // TODO Make names translateable static const std::map keyMap { - {Key::BackHold, {"Back Hold", makeSpecialKeyEventSequence(to_integral(Key::BackHold))}}, - {Key::NextHold, {"Next Hold", makeSpecialKeyEventSequence(to_integral(Key::NextHold))}}, + {Key::BackHoldMove, {"Back Hold Move", makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove))}}, + {Key::NextHoldMove, {"Next Hold Move", makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove))}}, }; return keyMap; } diff --git a/src/deviceinput.h b/src/deviceinput.h index b6d14c02..a22f5051 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -78,14 +78,17 @@ QDebug operator<<(QDebug debug, const KeyEvent &ke); // ------------------------------------------------------------------------------------------------- // Some inputs from Logitech Spotlight device (like Next Hold and Back Hold events) are not a valid // input event (input_event in linux/input.h) in a conventional sense. They are communicated -// via HID++ messages from the device. To use to InputMapper architechture in that case we need to -// reserve some KeyEventSequence for such events. These KeyEventSequence should be designed in +// via HID++ messages from the device. Using the input mapper we need to +// reserve some KeyEventSequence for theese events. These KeyEventSequence should be designed in // such a way that they cannot interfere with other valid input events from the device. namespace SpecialKeys { + constexpr uint16_t range = 0x0f00; // 0x0f00 - 0x0fff + constexpr uint16_t userRange = 0x0e00; // 0x0e00 - 0x0eff + enum class Key : uint16_t { - NextHold = 0x0ff0, - BackHold = 0x0ff1, + NextHoldMove = 0x0ff0, + BackHoldMove = 0x0ff1, }; struct SpecialKeyEventSeqInfo { diff --git a/src/spotlight.cc b/src/spotlight.cc index e55ecc01..646a3776 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -32,29 +32,46 @@ namespace { // ------------------------------------------------------------------------------------------------- -struct HoldButtonStatus { - enum class HoldButtonType : uint8_t { None, Next, Back }; - - void setButton(HoldButtonType b){ _button = b; _numEvents=0; }; - auto getButton() const { return _button; } - int numEvents() const { return _numEvents; }; - void addEvent(){ _numEvents++; }; - void reset(){ setButton(HoldButtonType::None); }; - auto keyEventSeq() { - switch (_button){ - case HoldButtonType::Next: - return SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold).keyEventSeq; - case HoldButtonType::Back: - return SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold).keyEventSeq; - case HoldButtonType::None: - return KeyEventSequence(); - } - return KeyEventSequence(); +// Hold button state. Very much Logitech Spotlight specific. +struct HoldButtonStatus +{ + enum class Button : uint16_t { + Next = 0x0e10, // must be in SpecialKeys user range + Back = 0x0e11, // must be in SpecialKeys user range + }; + + void setButtonsPressed(bool nextPressed, bool backPressed) + { + if (!m_nextPressed && nextPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove).keyEventSeq; + } else if (!m_backPressed && backPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove).keyEventSeq; + } else if (m_nextPressed && !nextPressed && backPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove).keyEventSeq; + } else if (m_backPressed && !backPressed && nextPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove).keyEventSeq; + } + + m_nextPressed = nextPressed; + m_backPressed = backPressed; + + if (!nextPressed && !backPressed) m_moveKeyEvSeq.clear(); + } + + bool nextPressed() const { return m_nextPressed; } + bool backPressed() const { return m_backPressed; } + + void reset() { m_nextPressed = m_backPressed = false; m_moveKeyEvSeq.clear(); }; + + const KeyEventSequence& moveKeyEventSeq() const { + return m_moveKeyEvSeq; }; private: - HoldButtonType _button = HoldButtonType::None; - unsigned long _numEvents = 0; + bool m_nextPressed = false; + bool m_backPressed = false; + + KeyEventSequence m_moveKeyEvSeq; }; // ------------------------------------------------------------------------------------------------- @@ -230,7 +247,7 @@ int Spotlight::connectDevices() connect(im, &InputMapper::actionMapped, this, [this](std::shared_ptr action) { - if (!(action->isRepeated()) && m_holdButtonStatus->numEvents() > 0) return; + // TODO allow hold button move events only with specific actions if (action->type() == Action::Type::CyclePresets) { @@ -418,7 +435,8 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) // Logitech button next and back press and hold + movement if (const auto rcIndex = connection->featureSet().featureIndex(FeatureCode::ReprogramControlsV4)) { - connection->registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) + connection->registerNotificationCallback(this, rcIndex, makeSafeCallback( + [this, connection](Message&& msg) { // Logitech Spotlight: // * Next Button = 0xda @@ -431,15 +449,21 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; - if (isNextPressed) { - m_holdButtonStatus->setButton(HoldButtonStatus::HoldButtonType::Next); - } else if (isBackPressed) { - m_holdButtonStatus->setButton(HoldButtonStatus::HoldButtonType::Back); - } else { - m_holdButtonStatus->reset(); + if (!m_holdButtonStatus->nextPressed() && isNextPressed + && !m_holdButtonStatus->backPressed() && isBackPressed) { + // TODO KeyEvnt with both presses at the same time + } + + if (!m_holdButtonStatus->nextPressed() && isNextPressed) { + connection->inputMapper()->addEvents(KeyEvent{{EV_KEY, to_integral(HoldButtonStatus::Button::Next), 1}}); } - }), 0 /* function 0 */); + if (!m_holdButtonStatus->backPressed() && isBackPressed) { + connection->inputMapper()->addEvents(KeyEvent{{EV_KEY, to_integral(HoldButtonStatus::Button::Back), 1}}); + } + + m_holdButtonStatus->setButtonsPressed(isNextPressed, isBackPressed); + }), 0 /* function 0 */); connection->registerNotificationCallback(this, rcIndex, makeSafeCallback([this, connection](Message&& msg) @@ -476,13 +500,13 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) static const auto volumeControlAction = GlobalActions::volumeControl(); volumeControlAction->param = -getReducedParam(y, 3); - // feed the keystroke to InputMapper and let it trigger the associated action - for (auto key_event: m_holdButtonStatus->keyEventSeq()) { - connection->inputMapper()->addEvents(key_event); + if (!connection->inputMapper()->recordingMode()) + { + // feed the keystroke to InputMapper and let it trigger the associated action + for (auto key_event : m_holdButtonStatus->moveKeyEventSeq()) { + connection->inputMapper()->addEvents(key_event); + } } - - m_holdButtonStatus->addEvent(); - }), 1 /* function 1 */); } } diff --git a/src/spotlight.h b/src/spotlight.h index 626d2023..59e75479 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -20,7 +20,6 @@ class SubHidppConnection; struct HoldButtonStatus; - /// Class handling spotlight device connections and indicating if a device is sending /// sending mouse move events. class Spotlight : public QObject, public async::Async From af5916d136d9ee74fa6c36a788468d6b10dba0fe Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 14 Sep 2021 19:03:38 +0200 Subject: [PATCH 068/110] Restrict action selection depending on input type. --- src/actiondelegate.cc | 5 +++-- src/device-defs.h | 26 ++++++++++++++++++++++++++ src/device-hidpp.cc | 8 ++++---- src/device-vibration.cc | 5 ++++- src/deviceinput.cc | 2 +- src/deviceinput.h | 2 +- src/devicescan.h | 38 +++++++++++++++++++------------------- src/deviceswidget.cc | 31 +++++++++++++++++++------------ src/deviceswidget.h | 24 ++++++++++++++++++------ src/inputmapconfig.cc | 20 ++++++++++++++++++++ src/inputseqedit.cc | 26 +++++++++++++++++++++++--- 11 files changed, 138 insertions(+), 49 deletions(-) diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index 87772eac..ed5bd4b0 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -350,7 +350,7 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* if (!item.action) return; const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); - const bool showRepeatedActions = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), + const bool isSpecialMoveInput = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), [&item](const auto& specialKeyInfo){ return (item.deviceSequence == specialKeyInfo.second.keyEventSeq); } @@ -396,7 +396,8 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* QMenu* menu = new QMenu(parent); for (const auto& entry : items) { - if (!entry.isRepeated || (entry.isRepeated && showRepeatedActions)) { + if ((isSpecialMoveInput && entry.isRepeated) + || (!isSpecialMoveInput && !entry.isRepeated)) { const auto qaction = menu->addAction(entry.icon, entry.text); connect(qaction, &QAction::triggered, this, [model, index, type=entry.type](){ model->setItemActionType(index, type); diff --git a/src/device-defs.h b/src/device-defs.h index ba9526d2..eccfdb45 100644 --- a/src/device-defs.h +++ b/src/device-defs.h @@ -5,9 +5,35 @@ #include +#include +#include + // Bus on which device is connected enum class BusType : uint8_t { Unknown, Usb, Bluetooth }; enum class ConnectionType : uint8_t { Event, Hidraw }; enum class ConnectionMode : uint8_t { ReadOnly, WriteOnly, ReadWrite }; + +// ------------------------------------------------------------------------------------------------- +struct DeviceId +{ + uint16_t vendorId = 0; + uint16_t productId = 0; + BusType busType = BusType::Unknown; + QString phys; // should be sufficient to differentiate between two devices of the same type + // - not tested, don't have two devices of any type currently. + + inline bool operator==(const DeviceId& rhs) const { + return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); + } + + inline bool operator!=(const DeviceId& rhs) const { + return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); + } + + inline bool operator<(const DeviceId& rhs) const { + return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); + } +}; +Q_DECLARE_METATYPE(DeviceId); diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index a671ec87..1598858d 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -704,12 +704,12 @@ void SubHidppConnection::updateDeviceFlags() } if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::ReprogramControlsV4)) { - auto& reservedInputs = m_inputMapper->reservedInputs(); - reservedInputs.clear(); + auto& specialInputs = m_inputMapper->specialInputs(); + specialInputs.clear(); featureFlagsSet |= DeviceFlags::NextHold; featureFlagsSet |= DeviceFlags::BackHold; - reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove)); - reservedInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove)); + specialInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove)); + specialInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove)); logDebug(hid) << tr("Subdevice '%1' reported %2 support.") .arg(path()).arg(toString(HIDPP::FeatureCode::ReprogramControlsV4)); } diff --git a/src/device-vibration.cc b/src/device-vibration.cc index c87e3a3c..fc39b17c 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -419,5 +419,8 @@ void VibrationSettingsWidget::sendVibrateCommand() const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); - m_subDeviceConnection->sendVibrateCommand(vint, vlen, {}); + m_subDeviceConnection->sendVibrateCommand(vint, vlen, + [](HidppConnectionInterface::MsgResult /* result */, HIDPP::Message&& /* msg */) { + // TODO debug log vibrate command reply? + }); } diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 8e363db8..19f198b9 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -831,7 +831,7 @@ const InputMapConfig& InputMapper::configuration() const } // ------------------------------------------------------------------------------------------------- -InputMapper::ReservedInputs& InputMapper::reservedInputs() +InputMapper::ReservedInputs& InputMapper::specialInputs() { return impl->m_reservedInputs; } diff --git a/src/deviceinput.h b/src/deviceinput.h index a22f5051..dad1334a 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -308,7 +308,7 @@ class InputMapper : public QObject void setKeyEventInterval(int interval); using ReservedInputs = std::vector; - ReservedInputs& reservedInputs(); + ReservedInputs& specialInputs(); std::shared_ptr virtualDevice() const; bool hasVirtualDevice() const; diff --git a/src/devicescan.h b/src/devicescan.h index a69df02f..368aaece 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -19,28 +19,28 @@ struct SupportedDevice QString name = {}; }; -// ------------------------------------------------------------------------------------------------- -struct DeviceId -{ - uint16_t vendorId = 0; - uint16_t productId = 0; - BusType busType = BusType::Unknown; - QString phys; // should be sufficient to differentiate between two devices of the same type - // - not tested, don't have two devices of any type currently. +// // ------------------------------------------------------------------------------------------------- +// struct DeviceId +// { +// uint16_t vendorId = 0; +// uint16_t productId = 0; +// BusType busType = BusType::Unknown; +// QString phys; // should be sufficient to differentiate between two devices of the same type +// // - not tested, don't have two devices of any type currently. - inline bool operator==(const DeviceId& rhs) const { - return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); - } +// inline bool operator==(const DeviceId& rhs) const { +// return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); +// } - inline bool operator!=(const DeviceId& rhs) const { - return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); - } +// inline bool operator!=(const DeviceId& rhs) const { +// return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); +// } - inline bool operator<(const DeviceId& rhs) const { - return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); - } -}; -Q_DECLARE_METATYPE(DeviceId); +// inline bool operator<(const DeviceId& rhs) const { +// return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); +// } +// }; +// Q_DECLARE_METATYPE(DeviceId); // ------------------------------------------------------------------------------------------------- namespace DeviceScan diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 5dee4fec..3089f0f9 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -250,18 +250,12 @@ void DevicesWidget::updateDeviceDetails(Spotlight* /* spotlight */) // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) { - const auto diWidget = new QWidget(this); - const auto layout = new QHBoxLayout(diWidget); - if (!m_deviceDetailsTextEdit) m_deviceDetailsTextEdit = new QTextEdit(this); - m_deviceDetailsTextEdit->setReadOnly(true); - m_deviceDetailsTextEdit->setText(""); - - updateDeviceDetails(spotlight); - connect(this, &DevicesWidget::currentDeviceChanged, this, [this, spotlight](){updateDeviceDetails(spotlight);}); - // TODO connect to deviceflag and battery status changes for the current device - // => only update info widget if something changes - - layout->addWidget(m_deviceDetailsTextEdit); + const auto diWidget = new DeviceInfoWidget(this); + + connect(this, &DevicesWidget::currentDeviceChanged, this, + [this, diWidget, spotlight](const DeviceId& dId) { + diWidget->setDeviceConnection(spotlight->deviceConnection(dId).get()); + }); return diWidget; } @@ -529,3 +523,16 @@ void TimerTabWidget::loadSettings(const DeviceId& deviceId) void TimerTabWidget::setSubDeviceConnection(SubDeviceConnection* sdc) { m_vibrationSettingsWidget->setSubDeviceConnection(sdc); } + +// ------------------------------------------------------------------------------------------------- +DeviceInfoWidget::DeviceInfoWidget(QWidget* parent) + : QWidget(parent) +{} + +void DeviceInfoWidget::setDeviceConnection(DeviceConnection* connection) +{ + if (m_connection == connection) return; + + m_connection = connection; + // TODO Update device information - connect signals. +} diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 76469b2b..40ffbf3c 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -2,23 +2,22 @@ // - See LICENSE.md and README.md #pragma once -#include "device.h" +#include "device-defs.h" #include #include +class DeviceConnection; class InputMapper; class MultiTimerWidget; class QComboBox; +class QTabWidget; class Settings; class Spotlight; class VibrationSettingsWidget; +class SubDeviceConnection; class TimerTabWidget; -class QTabWidget; -class QTimer; -class QTextEdit; - // ------------------------------------------------------------------------------------------------- class DevicesWidget : public QWidget { @@ -48,7 +47,7 @@ class DevicesWidget : public QWidget QWidget* m_deviceDetailsTabWidget = nullptr; // TODO Put into separate DeviceDetailsWidget - QTextEdit* m_deviceDetailsTextEdit = nullptr; + // QTextEdit* m_deviceDetailsTextEdit = nullptr; QPointer m_inputMapper; }; @@ -71,3 +70,16 @@ class TimerTabWidget : public QWidget MultiTimerWidget* m_multiTimerWidget = nullptr; VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; }; + +// ------------------------------------------------------------------------------------------------- +class DeviceInfoWidget : public QWidget +{ + Q_OBJECT + +public: + DeviceInfoWidget(QWidget* parent = nullptr); + void setDeviceConnection(DeviceConnection* connection); + +private: + QPointer m_connection; +}; \ No newline at end of file diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index 49f4e300..4b32c9f1 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -163,6 +163,26 @@ void InputMapConfigModel::setInputSequence(const QModelIndex& index, const KeyEv --m_duplicates[c.deviceSequence]; ++m_duplicates[kes]; c.deviceSequence = kes; + + const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); + const bool isSpecialMoveInput = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), + [&c](const auto& specialKeyInfo){ + return (c.deviceSequence == specialKeyInfo.second.keyEventSeq); + } + ); + + const bool isMoveAction = + (c.action->type() == Action::Type::ScrollHorizontal + || c.action->type() == Action::Type::ScrollVertical + || c.action->type() == Action::Type::VolumeControl); + + if (!isSpecialMoveInput && isMoveAction) { + setItemActionType(index, Action::Type::KeySequence); + } + else if (isSpecialMoveInput && !isMoveAction) { + setItemActionType(index, Action::Type::ScrollVertical); + } + configureInputMapper(); updateDuplicates(); emit dataChanged(index, index, {Qt::DisplayRole, Roles::InputSeqRole}); diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 9e485bbb..d4ff0c1e 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -519,15 +519,35 @@ void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* { if (!index.isValid() || !model) return; - const auto& reservedInputs = model->inputMapper()->reservedInputs(); - if (!reservedInputs.empty()) + const auto& specialInputs = model->inputMapper()->specialInputs(); + if (!specialInputs.empty()) { QMenu* menu = new QMenu(parent); - for (const auto& button : reservedInputs) { + for (const auto& button : specialInputs) { const auto qaction = menu->addAction(button.name); connect(qaction, &QAction::triggered, this, [model, index, button](){ model->setInputSequence(index, button.keyEventSeq); + const auto& currentItem = model->configData(index); + if (!currentItem.action) { + model->setItemActionType(index, Action::Type::ScrollHorizontal); + } + else + { + switch (currentItem.action->type()) + { + case Action::Type::ScrollHorizontal: // [[fallthrough]]; + case Action::Type::ScrollVertical: // [[fallthrough]]; + case Action::Type::VolumeControl: { + // scrolling and volume control allowed for special input + break; + } + default: { + model->setItemActionType(index, Action::Type::ScrollVertical); + break; + } + } + } }); } From 6961b5aa1e72b4016ad766795a5e77eae005d63c Mon Sep 17 00:00:00 2001 From: Jahn Date: Thu, 16 Sep 2021 21:03:30 +0200 Subject: [PATCH 069/110] Put device info widget into separate class. --- README.md | 16 +- src/actiondelegate.cc | 18 +- src/device-defs.h | 5 + src/device-hidpp.cc | 62 +++--- src/device-hidpp.h | 4 +- src/device.cc | 122 ++++++++--- src/device.h | 6 + src/deviceinput.cc | 18 +- src/deviceinput.h | 8 - src/deviceswidget.cc | 457 ++++++++++++++++++++++++++++++------------ src/deviceswidget.h | 53 ++++- src/enum-helper.h | 4 + src/hidpp.cc | 1 + src/hidpp.h | 6 +- src/preferencesdlg.cc | 5 - src/spotlight.cc | 67 +++++-- src/spotlight.h | 1 + 17 files changed, 581 insertions(+), 272 deletions(-) diff --git a/README.md b/README.md index 87762d50..ad4071ed 100644 --- a/README.md +++ b/README.md @@ -95,16 +95,10 @@ a button exists, _Projecteur_ will inject the mapped action instead. (You can still disable device grabbing with the `--disable-uinput` command line option - button mapping will be disabled then.) -Multiple mapped actions like Key Sequence, Cycle Preset etc. exist. The Key -Sequence action is particularly powerful as it can emit any user-defined +Input events from the presenter device can be mapped to different actions. +The _Key Sequence_ action is particularly powerful as it can emit any user-defined keystroke. These keystrokes can invoke shortcut in presentation software -being used. Some relevant shortcuts for presentations (support to these -shortcuts may vary between presentation softwares) are: - - * b or . : Toggle blank screen - * w or , : Toggle white screen - * F5 : Start presentation from the first slide - * Shift + F5 : Start presentation from the current slide +(or any other software) being used. #### Hold Button Mapping for Logitech Spotlight @@ -112,8 +106,8 @@ Logitech Spotlight can send Hold event for Next and Back buttons as HID++ messages. For mapping those inputs, please ensure that the device is active by pressing any button, then go to Input Mapping tab under Devices tab in Preferences dialog box and right click in first column (Input Sequence) for -any entry. Additional mapped actions (like Scrolling, volume control) can -be selected for such hold events. +any entry. Additional mapped actions (e.g. _Vertical Scrolling_ or _Volume control_) +can be selected for these special hold events. ## Download diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index ed5bd4b0..ad0737fc 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -360,17 +360,17 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* Action::Type type; QChar symbol; QString text; - bool isRepeated; + bool isMoveAction; QIcon icon = {}; }; static std::vector items { - {Action::Type::KeySequence, Font::Icon::keyboard_4, tr("Key Sequence"), KeySequenceAction().isRepeated()}, - {Action::Type::CyclePresets, Font::Icon::connection_8, tr("Cycle Presets"), CyclePresetsAction().isRepeated()}, - {Action::Type::ToggleSpotlight, Font::Icon::power_on_off_11, tr("Toggle Spotlight"), ToggleSpotlightAction().isRepeated()}, - {Action::Type::ScrollHorizontal, Font::Icon::cursor_21_rotated, tr("Scroll Horizontal"), ScrollHorizontalAction().isRepeated()}, - {Action::Type::ScrollVertical, Font::Icon::cursor_21, tr("Scroll Vertical"), ScrollVerticalAction().isRepeated()}, - {Action::Type::VolumeControl, Font::Icon::audio_6, tr("Volume Control"), VolumeControlAction().isRepeated()}, + {Action::Type::KeySequence, Font::Icon::keyboard_4, tr("Key Sequence"), false}, + {Action::Type::CyclePresets, Font::Icon::connection_8, tr("Cycle Presets"), false}, + {Action::Type::ToggleSpotlight, Font::Icon::power_on_off_11, tr("Toggle Spotlight"), false}, + {Action::Type::ScrollHorizontal, Font::Icon::cursor_21_rotated, tr("Scroll Horizontal"), true}, + {Action::Type::ScrollVertical, Font::Icon::cursor_21, tr("Scroll Vertical"), true}, + {Action::Type::VolumeControl, Font::Icon::audio_6, tr("Volume Control"), true}, }; static bool initIcons = []() @@ -396,8 +396,8 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* QMenu* menu = new QMenu(parent); for (const auto& entry : items) { - if ((isSpecialMoveInput && entry.isRepeated) - || (!isSpecialMoveInput && !entry.isRepeated)) { + if ((isSpecialMoveInput && entry.isMoveAction) + || (!isSpecialMoveInput && !entry.isMoveAction)) { const auto qaction = menu->addAction(entry.icon, entry.text); connect(qaction, &QAction::triggered, this, [model, index, type=entry.type](){ model->setItemActionType(index, type); diff --git a/src/device-defs.h b/src/device-defs.h index eccfdb45..7b071d57 100644 --- a/src/device-defs.h +++ b/src/device-defs.h @@ -15,6 +15,11 @@ enum class ConnectionType : uint8_t { Event, Hidraw }; enum class ConnectionMode : uint8_t { ReadOnly, WriteOnly, ReadWrite }; +// ------------------------------------------------------------------------------------------------- +const char* toString(BusType bt, bool withClass = true); +const char* toString(ConnectionType ct, bool withClass = true); +const char* toString(ConnectionMode cm, bool withClass = true); + // ------------------------------------------------------------------------------------------------- struct DeviceId { diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index 1598858d..a0e77ed2 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -30,28 +30,28 @@ SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token, SubHidppConnection::~SubHidppConnection() = default; // ------------------------------------------------------------------------------------------------- -const char* toString(SubHidppConnection::ReceiverState s) +const char* toString(SubHidppConnection::ReceiverState s, bool withClass) { using ReceiverState = SubHidppConnection::ReceiverState; switch (s) { - ENUM_CASE_STRINGIFY(ReceiverState::Uninitialized); - ENUM_CASE_STRINGIFY(ReceiverState::Initializing); - ENUM_CASE_STRINGIFY(ReceiverState::Initialized); - ENUM_CASE_STRINGIFY(ReceiverState::Error); + ENUM_CASE_STRINGIFY3(ReceiverState, Uninitialized, withClass); + ENUM_CASE_STRINGIFY3(ReceiverState, Initializing, withClass); + ENUM_CASE_STRINGIFY3(ReceiverState, Initialized, withClass); + ENUM_CASE_STRINGIFY3(ReceiverState, Error, withClass); } return "ReceiverState::(unknown)"; } -const char* toString(SubHidppConnection::PresenterState s) +const char* toString(SubHidppConnection::PresenterState s, bool withClass) { using PresenterState = SubHidppConnection::PresenterState; switch (s) { - ENUM_CASE_STRINGIFY(PresenterState::Uninitialized); - ENUM_CASE_STRINGIFY(PresenterState::Uninitialized_Offline); - ENUM_CASE_STRINGIFY(PresenterState::Initializing); - ENUM_CASE_STRINGIFY(PresenterState::Initialized_Online); - ENUM_CASE_STRINGIFY(PresenterState::Initialized_Offline); - ENUM_CASE_STRINGIFY(PresenterState::Error); + ENUM_CASE_STRINGIFY3(PresenterState, Uninitialized, withClass); + ENUM_CASE_STRINGIFY3(PresenterState, Uninitialized_Offline, withClass); + ENUM_CASE_STRINGIFY3(PresenterState, Initializing, withClass); + ENUM_CASE_STRINGIFY3(PresenterState, Initialized_Online, withClass); + ENUM_CASE_STRINGIFY3(PresenterState, Initialized_Offline, withClass); + ENUM_CASE_STRINGIFY3(PresenterState, Error, withClass); } return "PresenterState::(unknown)"; } @@ -408,7 +408,7 @@ void SubHidppConnection::getBatteryLevelStatus( { if (!cb) return; - auto batteryInfo = (res == MsgResult::Ok) ? BatteryInfo{} + auto batteryInfo = (res != MsgResult::Ok) ? BatteryInfo{} : BatteryInfo{msg[4], msg[5], to_enum(msg[6])}; @@ -741,7 +741,6 @@ void SubHidppConnection::registerForFeatureNotifications() { registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) { - // TODO implement button hold states // Logitech Spotlight: // * Next Button = 0xda // * Back Button = 0xdc @@ -753,27 +752,19 @@ void SubHidppConnection::registerForFeatureNotifications() const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; logDebug(hid) << tr("Buttons pressed: Next = %1, Back = %2") - .arg(isNextPressed).arg(isBackPressed) << msg.hex(); + .arg(isNextPressed).arg(isBackPressed); }), 0 /* function 0 */); - registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) - { - // TODO Implement hold and move logic and bindings - Q_UNUSED(msg); - - // byte 4 : -1 for left movement, 0 for right movement - // byte 5 : horizontal movement speed -128 to 127 - // byte 6 : -1 for up movement, 0 for down movement - // byte 7 : vertical movement speed -128 to 127 - - // auto cast = [](uint8_t v) -> int{ return static_cast(v); }; - // logDebug(hid) << tr("4 = %1, 5 = %2, 6 = %3, 7 = %4") - // .arg(cast(msg[4]), 4) - // .arg(cast(msg[5]), 4) - // .arg(cast(msg[6]), 4) - // .arg(cast(msg[7]), 4); - }), 1 /* function 1 */); + // Handling of move events by button hold is done in spotlight.cc + // The following commented out code is kept as example + + // registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) { + // byte 4 : -1 for left movement, 0 for right movement + // byte 5 : horizontal movement speed -128 to 127 + // byte 6 : -1 for up movement, 0 for down movement + // byte 7 : vertical movement speed -128 to 127 + // }), 1 /* function 1 */); } if (const auto batIndex = m_featureSet.featureIndex(FeatureCode::BatteryStatus)) @@ -796,7 +787,10 @@ void SubHidppConnection::registerForUsbNotifications() .arg(toString(HIDPP::Notification::DeviceConnection)).arg(linkEstablished); if (!linkEstablished) { - // TODO no link to device => depending on current state set new presenter state + if (m_presenterState == PresenterState::Initialized_Online) { + setPresenterState(PresenterState::Initialized_Offline); + } + logInfo(hid) << tr("HID++ device '%1' went offline.").arg(path()); return; } @@ -805,7 +799,7 @@ void SubHidppConnection::registerForUsbNotifications() || m_presenterState == PresenterState::Uninitialized || m_presenterState == PresenterState::Error) { - logInfo(hid) << tr("Device '%1' came online.").arg(path()); + logInfo(hid) << tr("HID++ device '%1' came online.").arg(path()); checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState /* ps */) { //... })); diff --git a/src/device-hidpp.h b/src/device-hidpp.h index 1f5265c8..fbebb2e6 100644 --- a/src/device-hidpp.h +++ b/src/device-hidpp.h @@ -133,5 +133,5 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt std::unordered_map> m_notificationSubscribers; }; -const char* toString(SubHidppConnection::ReceiverState rs); -const char* toString(SubHidppConnection::PresenterState ps); +const char* toString(SubHidppConnection::ReceiverState rs, bool withClass = true); +const char* toString(SubHidppConnection::PresenterState ps, bool withClass = true); diff --git a/src/device.cc b/src/device.cc index 946c7335..eb9cfad1 100644 --- a/src/device.cc +++ b/src/device.cc @@ -3,6 +3,7 @@ #include "device.h" +#include "enum-helper.h" #include "deviceinput.h" #include "devicescan.h" #include "hidpp.h" @@ -26,6 +27,38 @@ namespace { // class i18n : public QObject {}; // for i18n and logging } +// ------------------------------------------------------------------------------------------------- +const char* toString(BusType bt, bool withClass) +{ + switch (bt) { + ENUM_CASE_STRINGIFY3(BusType, Unknown, withClass); + ENUM_CASE_STRINGIFY3(BusType, Usb, withClass); + ENUM_CASE_STRINGIFY3(BusType, Bluetooth, withClass); + } + return withClass ? "BusType::(unknown)" : "(unkown)"; +} + +// ------------------------------------------------------------------------------------------------- +const char* toString(ConnectionType ct, bool withClass) +{ + switch (ct) { + ENUM_CASE_STRINGIFY3(ConnectionType, Event, withClass); + ENUM_CASE_STRINGIFY3(ConnectionType, Hidraw, withClass); + } + return withClass ? "ConnectionType::(unknown)" : "(unkown)"; +} + +// ------------------------------------------------------------------------------------------------- +const char* toString(ConnectionMode cm, bool withClass) +{ + switch (cm) { + ENUM_CASE_STRINGIFY3(ConnectionMode, ReadOnly, withClass); + ENUM_CASE_STRINGIFY3(ConnectionMode, WriteOnly, withClass); + ENUM_CASE_STRINGIFY3(ConnectionMode, ReadWrite, withClass); + } + return withClass ? "ConnectionMode::(unknown)" : "(unkown)"; +} + // ------------------------------------------------------------------------------------------------- DeviceConnection::DeviceConnection(const DeviceId& id, const QString& name, std::shared_ptr vdev) @@ -76,6 +109,16 @@ bool DeviceConnection::removeSubDevice(const QString& path) return false; } +// ------------------------------------------------------------------------------------------------- +std::shared_ptr DeviceConnection::subDevice(const QString& devicePath) const +{ + const auto it = m_subDeviceConnections.find(devicePath); + if (it == m_subDeviceConnections.cend()) { + return {}; + } + + return it->second; +} // ------------------------------------------------------------------------------------------------- bool DeviceConnection::hasHidppSupport() const { @@ -83,39 +126,6 @@ bool DeviceConnection::hasHidppSupport() const { return m_deviceId.vendorId == 0x046d; } -// // ------------------------------------------------------------------------------------------------- -// void DeviceConnection::queryBatteryStatus() -// { -// for (const auto& sd: subDevices()) -// { -// if (sd.second->type() == ConnectionType::Hidraw -// && sd.second->mode() == ConnectionMode::ReadWrite) -// { -// if (sd.second->hasFlags(DeviceFlag::ReportBattery)) sd.second->queryBatteryStatus(); -// } -// } -// } - -// // ------------------------------------------------------------------------------------------------- -// void DeviceConnection::setBatteryInfo(const QByteArray& batteryData) -// { -// // TODO Refactor battery handling -// const bool hasBattery = -// std::any_of(m_subDeviceConnections.cbegin(), m_subDeviceConnections.cend(), [](const auto& sd) { -// return sd.second->hasFlags(DeviceFlag::ReportBattery); -// }); - -// if (hasBattery && batteryData.length() == 3) -// { -// // Battery percent is only meaningful when battery is discharging. However, save them anyway. -// m_batteryInfo.currentLevel -// = static_cast(batteryData.at(0) <= 100 ? batteryData.at(0) : 100); -// m_batteryInfo.nextReportedLevel -// = static_cast(batteryData.at(1) <= 100 ? batteryData.at(1): 100); -// m_batteryInfo.status = static_cast((batteryData.at(2) <= 0x07) ? batteryData.at(2): 0x07); -// } -// } - // ------------------------------------------------------------------------------------------------- SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode) @@ -420,3 +430,49 @@ void SubHidrawConnection::onHidrawDataAvailable(int fd) // received data into the debug log logDebug(hid) << "Received" << readVal.toHex() << "from" << path(); } + +// ------------------------------------------------------------------------------------------------- +const char* toString(DeviceFlag f, bool withClass) +{ + switch(f) { + ENUM_CASE_STRINGIFY3(DeviceFlag, NoFlags, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, NonBlocking, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, SynEvents, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, RepEvents, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, RelativeEvents, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, KeyEvents, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, Hidpp, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, Vibrate, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, ReportBattery, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, NextHold, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, BackHold, withClass); + ENUM_CASE_STRINGIFY3(DeviceFlag, PointerSpeed, withClass); + } + return withClass ? "DeviceFlag::(unknown)" : "(unknown)"; +} + +// ------------------------------------------------------------------------------------------------- +QString toString(DeviceFlags flags, const QString& separator, bool withClass) +{ + return toStringList(flags, withClass).join(separator); +} + +// ------------------------------------------------------------------------------------------------- +QStringList toStringList(DeviceFlags flags, bool withClass) +{ + if (flags == DeviceFlags::NoFlags) { + return QStringList{ ENUM_STRINGIFY3(DeviceFlag, NoFlags, withClass) }; + } + + QStringList list; + + for (size_t i = 0; i < sizeof(std::underlying_type_t) * 8; ++i) + { + const std::underlying_type_t singleFlag = 1 << i; + if ((to_integral(flags) & singleFlag) == singleFlag) { + list.push_back(toString(to_enum(singleFlag))); + } + } + + return list; +} diff --git a/src/device.h b/src/device.h index cc2e0a4f..d9a9a8c5 100644 --- a/src/device.h +++ b/src/device.h @@ -41,6 +41,7 @@ class DeviceConnection : public QObject void addSubDevice(std::shared_ptr); bool removeSubDevice(const QString& path); const auto& subDevices() { return m_subDeviceConnections; } + std::shared_ptr subDevice(const QString& devicePath) const; signals: void subDeviceConnected(const DeviceId& id, const QString& path); @@ -75,6 +76,11 @@ enum class DeviceFlag : uint32_t { }; ENUM(DeviceFlag, DeviceFlags) +// ------------------------------------------------------------------------------------------------- +const char* toString(DeviceFlag flag, bool withClass = true); +QString toString(DeviceFlags flags, const QString& separator, bool withClass = true); +QStringList toStringList(DeviceFlags flags, bool withClass = true); + // ------------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { SubDeviceConnectionDetails(const DeviceId& dId, const DeviceScan::SubDevice& sd, diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 19f198b9..329a67ea 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -551,14 +551,8 @@ void InputMapper::Impl::execAction(const std::shared_ptr& action, Device { if (!action || action->empty()) return; - if (action->isRepeated()) - { - if(m_parent->m_repeatedActionTimer->isActive()) return; - m_parent->m_repeatedActionTimer->start(); - } - logDebug(input) << "Input map action, type = " << int(action->type()) - << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit) << action->isRepeated(); + << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit); if (action->type() == Action::Type::KeySequence) { @@ -647,16 +641,10 @@ void InputMapper::Impl::record(const struct input_event input_events[], size_t n InputMapper::InputMapper(std::shared_ptr virtualDevice, QObject* parent) : QObject(parent) , impl(std::make_unique(this, std::move(virtualDevice))) - , m_repeatedActionTimer(new QTimer(this)) -{ - m_repeatedActionTimer->setSingleShot(true); - m_repeatedActionTimer->setInterval(25); // time gap between repeated action -} +{} // ------------------------------------------------------------------------------------------------- -InputMapper::~InputMapper() -{ -} +InputMapper::~InputMapper() {} // ------------------------------------------------------------------------------------------------- std::shared_ptr InputMapper::virtualDevice() const diff --git a/src/deviceinput.h b/src/deviceinput.h index dad1334a..82a52fa4 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -181,7 +181,6 @@ struct Action virtual QDataStream& save(QDataStream&) const = 0; virtual QDataStream& load(QDataStream&) = 0; virtual bool empty() const = 0; - virtual bool isRepeated() const = 0; }; // ------------------------------------------------------------------------------------------------- @@ -193,7 +192,6 @@ struct KeySequenceAction : public Action QDataStream& save(QDataStream& s) const override { return s << keySequence; } QDataStream& load(QDataStream& s) override { return s >> keySequence; } bool empty() const override { return keySequence.empty(); } - bool isRepeated() const override { return false; } bool operator==(const KeySequenceAction& o) const { return keySequence == o.keySequence; } NativeKeySequence keySequence; @@ -206,7 +204,6 @@ struct CyclePresetsAction : public Action QDataStream& save(QDataStream& s) const override { return s << placeholder; } QDataStream& load(QDataStream& s) override { return s >> placeholder; } bool empty() const override { return false; } - bool isRepeated() const override { return false; } bool operator==(const CyclePresetsAction&) const { return true; } bool placeholder = false; }; @@ -218,7 +215,6 @@ struct ToggleSpotlightAction : public Action QDataStream& save(QDataStream& s) const override { return s << placeholder; } QDataStream& load(QDataStream& s) override { return s >> placeholder; } bool empty() const override { return false; } - bool isRepeated() const override { return false; } bool operator==(const ToggleSpotlightAction&) const { return true; } bool placeholder = false; }; @@ -230,7 +226,6 @@ struct ScrollHorizontalAction : public Action QDataStream& save(QDataStream& s) const override { return s << placeholder; } QDataStream& load(QDataStream& s) override { return s >> placeholder; } bool empty() const override { return false; } - bool isRepeated() const override { return true; } bool operator==(const ScrollHorizontalAction&) const { return true; } bool placeholder = false; @@ -244,7 +239,6 @@ struct ScrollVerticalAction : public Action QDataStream& save(QDataStream& s) const override { return s << placeholder; } QDataStream& load(QDataStream& s) override { return s >> placeholder; } bool empty() const override { return false; } - bool isRepeated() const override { return true; } bool operator==(const ScrollVerticalAction&) const { return true; } bool placeholder = false; @@ -258,7 +252,6 @@ struct VolumeControlAction : public Action QDataStream& save(QDataStream& s) const override { return s << placeholder; } QDataStream& load(QDataStream& s) override { return s >> placeholder; } bool empty() const override { return false; } - bool isRepeated() const override { return true; } bool operator==(const VolumeControlAction&) const { return true; } bool placeholder = false; @@ -331,5 +324,4 @@ class InputMapper : public QObject private: struct Impl; std::unique_ptr impl; - QTimer* m_repeatedActionTimer = nullptr; // Timer for introducing time gap between repeated ations }; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 3089f0f9..ef885da2 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -3,7 +3,7 @@ #include "deviceswidget.h" -#include "device.h" +#include "device-hidpp.h" #include "device-vibration.h" #include "deviceinput.h" #include "iconwidgets.h" @@ -22,6 +22,7 @@ #include #include #include +#include DECLARE_LOGGING_CATEGORY(preferences) @@ -122,131 +123,6 @@ QWidget* DevicesWidget::createDevicesWidget(Settings* settings, Spotlight* spotl return dw; } -// ------------------------------------------------------------------------------------------------- -void DevicesWidget::updateDeviceDetails(Spotlight* /* spotlight */) -{ - // // TODO refactor device details together with battery info handling - // auto updateBatteryInfo = [this, spotlight]() { - // auto curDeviceId = currentDeviceId(); - // if (curDeviceId == invalidDeviceId) - // return; - // auto dc = spotlight->deviceConnection(curDeviceId); - // dc->queryBatteryStatus(); - // }; - - // auto getDeviceDetails = [this, spotlight]() { - // QString deviceDetails; - // auto curDeviceId = currentDeviceId(); - // if (curDeviceId == invalidDeviceId) - // return tr("No Device Connected"); - // auto dc = spotlight->deviceConnection(curDeviceId); - - // const auto busTypeToString = [](BusType type) -> QString { - // if (type == BusType::Usb) return "USB"; - // if (type == BusType::Bluetooth) return "Bluetooth"; - // return "Unknown"; - // }; - - // const QStringList subDeviceList = [dc](){ - // QStringList subDeviceList; - // auto accessText = [](ConnectionMode m){ - // if (m == ConnectionMode::ReadOnly) return "ReadOnly"; - // if (m == ConnectionMode::WriteOnly) return "WriteOnly"; - // if (m == ConnectionMode::ReadWrite) return "ReadWrite"; - // return "Unknown Access"; - // }; - // for (const auto& sd: dc->subDevices()) { - // if (sd.second->path().size()) { - // auto sds = sd.second; - // subDeviceList.push_back(tr("%1%2[%3, %4, %5]").arg( - // sds->path(), - // (sds->path().length()<18)?"\t\t":"\t", - // accessText(sds->mode()), - // sds->isGrabbed()?"Grabbed":"", - // sds->hasFlags(DeviceFlags::Hidpp)?"HID++":"" - // )); - // } - // } - // return subDeviceList; - // }(); - // auto batteryStatusText = [](BatteryStatus d){ - // if (d == BatteryStatus::Discharging) return "Discharging"; - // if (d == BatteryStatus::Charging) return "Charging"; - // if (d == BatteryStatus::AlmostFull) return "Almost Full"; - // if (d == BatteryStatus::Full) return "Full Charge"; - // if (d == BatteryStatus::SlowCharging) return "Slow Charging"; - // if (d == BatteryStatus::InvalidBattery || d == BatteryStatus::ThermalError || d == BatteryStatus::ChargingError) { - // return "Charging Error"; - // }; - // return ""; - // }; - - // auto sDevices = dc->subDevices(); - // bool isOnline = false, hasBattery = false, hasHIDPP = false; - // float HIDPPversion = -1; - // QStringList HIDPPfeatureText; - // auto HIDppSubDevice = std::find_if(sDevices.cbegin(), sDevices.cend(), [](const auto& sd){ - // return (sd.second->type() == ConnectionType::Hidraw && - // sd.second->mode() == ConnectionMode::ReadWrite && - // sd.second->hasFlags(DeviceFlag::Hidpp)); - // }); - - // if (HIDppSubDevice != sDevices.cend()) - // { - // auto dev = HIDppSubDevice->second; - // hasHIDPP = true; - // isOnline = dev->isOnline(); - // hasBattery = dev->hasFlags(DeviceFlags::ReportBattery); - // HIDPPversion = dev->getHIDppProtocol(); - // // report HID++ features recognised by program (like vibration and others) - // HIDPPfeatureText = [dev](){ - // QStringList flagList; - // if (dev->hasFlags(DeviceFlag::Vibrate)) flagList.push_back("Vibration"); - // if (dev->hasFlags(DeviceFlag::ReportBattery)) flagList.push_back("Reports Battery"); - // if (dev->hasFlags(DeviceFlag::NextHold)) flagList.push_back("Next Hold Button (Reprogrammed)"); - // if (dev->hasFlags(DeviceFlag::BackHold)) flagList.push_back("Back Hold Button (Reprogrammed)"); - // if (dev->hasFlags(DeviceFlag::PointerSpeed)) flagList.push_back("Variable Pointer Speed"); - // return flagList; - // }(); - // } - - // auto batteryInfoText = [dc, batteryStatusText](){ - // auto batteryInfo= dc->getBatteryInfo(); - // // Only show battery percent while discharging. - // // Other cases, device do not report battery percentage correctly. - // if (batteryInfo.status == BatteryStatus::Discharging) { - // return tr("%1\% - %2% (%3)").arg( - // QString::number(batteryInfo.currentLevel), - // QString::number(batteryInfo.nextReportedLevel), - // batteryStatusText(batteryInfo.status)); - // } else { - // return tr("%3").arg(batteryStatusText(batteryInfo.status)); - // } - // }; - - // deviceDetails += tr("Name:\t\t%1\n").arg(dc->deviceName()); - // deviceDetails += tr("VendorId:\t%1\n").arg(hexId(dc->deviceId().vendorId)); - // deviceDetails += tr("ProductId:\t%1\n").arg(hexId(dc->deviceId().productId)); - // deviceDetails += tr("Phys:\t\t%1\n").arg(dc->deviceId().phys); - // deviceDetails += tr("Bus Type:\t%1\n").arg(busTypeToString(dc->deviceId().busType)); - // deviceDetails += tr("Sub-Devices:\t%1\n").arg(subDeviceList.join(",\n\t\t")); - // if (hasBattery && isOnline) deviceDetails += tr("Battery Status:\t%1\n").arg(batteryInfoText()); - // if (hasHIDPP && !isOnline) deviceDetails += tr("\n\n\t Device not active. Press any key on device to update.\n"); - // if (hasHIDPP && isOnline){ - // deviceDetails += "\n"; - // deviceDetails += tr("HID++ Version:\t%1\n").arg(HIDPPversion); - // deviceDetails += tr("HID++ Features:\t%1\n").arg(HIDPPfeatureText.join(",\n\t\t")); - // } - - // return deviceDetails; - // }; - - // QTimer::singleShot(200, this, [updateBatteryInfo](){updateBatteryInfo();}); - // if (m_deviceDetailsTextEdit) { - // QTimer::singleShot(1000, this, [this, getDeviceDetails](){m_deviceDetailsTextEdit->setText(getDeviceDetails());}); - // } -} - // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) { @@ -256,6 +132,8 @@ QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) [this, diWidget, spotlight](const DeviceId& dId) { diWidget->setDeviceConnection(spotlight->deviceConnection(dId).get()); }); + + diWidget->setDeviceConnection(spotlight->deviceConnection(currentDeviceId()).get()); return diWidget; } @@ -483,7 +361,6 @@ void DevicesWidget::updateTimerTab(Spotlight* spotlight) removeTab(m_tabWidget, m_timerTabWidget); m_timerTabWidget->setSubDeviceConnection(nullptr); } - m_tabWidget->setCurrentIndex(0); if (currentConn) { m_timerTabContext = QPointer(new QObject(this)); @@ -527,12 +404,334 @@ void TimerTabWidget::setSubDeviceConnection(SubDeviceConnection* sdc) { // ------------------------------------------------------------------------------------------------- DeviceInfoWidget::DeviceInfoWidget(QWidget* parent) : QWidget(parent) -{} + , m_textEdit(new QTextEdit(this)) + , m_delayedUpdateTimer(new QTimer(this)) + , m_batteryInfoTimer(new QTimer(this)) +{ + m_textEdit->setReadOnly(true); + + const auto layout = new QVBoxLayout(this); + layout->addWidget(m_textEdit); + m_delayedUpdateTimer->setSingleShot(true); + m_delayedUpdateTimer->setInterval(150); + connect(m_delayedUpdateTimer, &QTimer::timeout, this, &DeviceInfoWidget::updateTextEdit); + + m_batteryInfoTimer->setSingleShot(false); + m_batteryInfoTimer->setTimerType(Qt::VeryCoarseTimer); + m_batteryInfoTimer->setInterval(5 * 60 * 1000); // 5 minutes +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::delayedTextEditUpdate() { + m_delayedUpdateTimer->start(); +} + +// ------------------------------------------------------------------------------------------------- void DeviceInfoWidget::setDeviceConnection(DeviceConnection* connection) { if (m_connection == connection) return; + if (m_connectionContext) { m_connectionContext->deleteLater(); } m_connection = connection; - // TODO Update device information - connect signals. + + if (m_connection.isNull()) + { + m_delayedUpdateTimer->stop(); + m_batteryInfoTimer->stop(); + m_textEdit->clear(); + return; + } + + m_connectionContext = new QObject(this); + + m_deviceBaseInfo.clear(); + m_deviceBaseInfo.emplace_back("Name", m_connection->deviceName()); + m_deviceBaseInfo.emplace_back("VendorId", hexId(m_connection->deviceId().vendorId)); + m_deviceBaseInfo.emplace_back("ProductId", hexId(m_connection->deviceId().productId)); + m_deviceBaseInfo.emplace_back("Phys", m_connection->deviceId().phys); + m_deviceBaseInfo.emplace_back("Bus Type", toString(m_connection->deviceId().busType, false)); + + connect(m_connection, &DeviceConnection::subDeviceConnected, m_connectionContext, + [this](const DeviceId&, const QString& path) + { + if (const auto sdc = m_connection->subDevice(path)) + { + updateSubdeviceInfo(sdc.get()); + connectToSubdeviceUpdates(sdc.get()); + delayedTextEditUpdate(); + } + }); + + connect(m_connection, &DeviceConnection::subDeviceConnected, m_connectionContext, + [this](const DeviceId&, const QString& path) + { + const auto it = m_subDevices.find(path); + if (it == m_subDevices.cend()) { + return; + } + + if (it->second.isHidpp) { + m_hidppInfo.clear(); + } + + if (it->second.hasBatteryInfo) { + m_batteryInfo.clear(); + m_batteryInfoTimer->stop(); + } + + m_subDevices.erase(it); + delayedTextEditUpdate(); + }); + + initSubdeviceInfo(); + updateTextEdit(); +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::connectToBatteryUpdates(SubHidppConnection* hdc) +{ + if (hdc->hasFlags(DeviceFlag::ReportBattery)) + { + connect(hdc, &SubHidppConnection::batteryInfoChanged, m_connectionContext, [this, hdc]() { + updateBatteryInfo(hdc); + m_batteryInfoTimer->start(); + delayedTextEditUpdate(); + }); + + connect(m_batteryInfoTimer, &QTimer::timeout, m_connectionContext, [this, hdc]() { + hdc->triggerBattyerInfoUpdate(); + }); + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::connectToSubdeviceUpdates(SubDeviceConnection* sdc) +{ + connect(sdc, &SubDeviceConnection::flagsChanged, m_connectionContext, [this, sdc]() + { + if (!m_subDevices[sdc->path()].hasBatteryInfo + && sdc->hasFlags(DeviceFlag::ReportBattery)) + { + if (const auto hdc = qobject_cast(sdc)) { + connectToBatteryUpdates(hdc); + hdc->triggerBattyerInfoUpdate(); + } + } + + updateSubdeviceInfo(sdc); + if (const auto hdc = qobject_cast(sdc)) { + updateHidppInfo(hdc); + delayedTextEditUpdate(); + } + }); + + // HID++ device only updates + if (const auto hdc = qobject_cast(sdc)) + { + connectToBatteryUpdates(hdc); + + if (hdc->busType() == BusType::Usb) + { + connect(hdc, &SubHidppConnection::receiverStateChanged, m_connectionContext, + [this](SubHidppConnection::ReceiverState s) { + m_hidppInfo.receiverState = toString(s, false); + delayedTextEditUpdate(); + }); + } + + connect(hdc, &SubHidppConnection::presenterStateChanged, m_connectionContext, + [this, hdc](SubHidppConnection::PresenterState s) { + m_hidppInfo.presenterState = toString(s, false); + const auto pv = hdc->protocolVersion(); + m_hidppInfo.protocolVersion = QString("%1.%2").arg(pv.major).arg(pv.minor); + delayedTextEditUpdate(); + }); + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateTextEdit() +{ + m_textEdit->clear(); + + QTextCharFormat normalFormat; + normalFormat.setFontUnderline(false); + QTextCharFormat underlineFormat; + underlineFormat.setFontUnderline(true); + QTextCharFormat italicFormat; + italicFormat.setFontItalic(true); + + auto cursor = m_textEdit->textCursor(); + + { // Insert table with basic device information + QTextTableFormat tableFormat; + tableFormat.setBorder(1); + tableFormat.setCellSpacing(0); + tableFormat.setBorderBrush(QBrush(Qt::lightGray)); + tableFormat.setCellPadding(2); + tableFormat.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + cursor.insertTable(m_deviceBaseInfo.size(), 2, tableFormat); + + for (const auto& info : m_deviceBaseInfo) + { + cursor.insertText(info.first, italicFormat); + cursor.movePosition(QTextCursor::NextCell); + cursor.insertText(info.second, normalFormat); + cursor.movePosition(QTextCursor::NextCell); + } + cursor.movePosition(QTextCursor::End); + } + + { // Insert list of sub devices + cursor.insertBlock(); + cursor.insertBlock(); + cursor.insertText(tr("Sub devices:"), underlineFormat); + cursor.insertText(" ", normalFormat); + cursor.insertBlock(); + cursor.movePosition(QTextCursor::PreviousBlock); + cursor.movePosition(QTextCursor::EndOfBlock); + cursor.setBlockCharFormat(normalFormat); + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + listFormat.setIndent(1); + cursor.insertList(listFormat); + + for (const auto& subDeviceInfo : m_subDevices) { + cursor.insertText(subDeviceInfo.first); + cursor.insertText(": "); + cursor.insertText(subDeviceInfo.second.info); + if (cursor.currentList()->itemNumber(cursor.block()) + < static_cast(m_subDevices.size() - 1)) { + cursor.insertBlock(); + } + } + cursor.movePosition(QTextCursor::MoveOperation::NextBlock); + } + + if (!m_batteryInfo.isEmpty()) { + cursor.insertBlock(); + cursor.insertText(tr("Battery Info:"), underlineFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_batteryInfo); + cursor.insertBlock(); + } + + if (!m_hidppInfo.presenterState.isEmpty()) + { + cursor.insertBlock(); + cursor.insertText(tr("HID++ Info:"), underlineFormat); + cursor.insertText(" ", normalFormat); + cursor.insertBlock(); + cursor.movePosition(QTextCursor::PreviousBlock); + cursor.movePosition(QTextCursor::EndOfBlock); + cursor.setBlockCharFormat(normalFormat); + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + listFormat.setIndent(1); + cursor.insertList(listFormat); + + if (!m_hidppInfo.receiverState.isEmpty()) { + cursor.insertText(tr("Receiver state:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.receiverState); + } + + cursor.insertBlock(); + cursor.insertText(tr("Presenter state:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.presenterState); + + cursor.insertBlock(); + cursor.insertText(tr("Protocol version:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.protocolVersion); + + cursor.insertBlock(); + cursor.insertText(tr("Supported features:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.hidppFlags.join(", ")); + + cursor.movePosition(QTextCursor::MoveOperation::NextBlock); + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateSubdeviceInfo(SubDeviceConnection* sdc) +{ + const auto hdc = qobject_cast(sdc); + m_subDevices[sdc->path()] = SubDeviceInfo{ + QString("[%2%3%4]").arg( + toString(sdc->mode(), false), + sdc->isGrabbed() ? ", Grabbed" : "", + sdc->hasFlags(DeviceFlag::Hidpp) ? ", HID++" : ""), + hdc != nullptr, + (hdc != nullptr) ? hdc->hasFlags(DeviceFlag::ReportBattery) : false + }; +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::initSubdeviceInfo() +{ + m_subDevices.clear(); + m_batteryInfo.clear(); + m_batteryInfoTimer->stop(); + m_hidppInfo.clear(); + + for (const auto& sd : m_connection->subDevices()) + { + const auto& sdc = sd.second; + if (sdc->path().isEmpty()) continue; + updateSubdeviceInfo(sdc.get()); + connectToSubdeviceUpdates(sdc.get()); + + if (const auto hdc = qobject_cast(sdc.get())) + { + updateHidppInfo(hdc); + + if (hdc->hasFlags(DeviceFlag::ReportBattery)) { + updateBatteryInfo(hdc); + hdc->triggerBattyerInfoUpdate(); + } + } + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateHidppInfo(SubHidppConnection* hdc) +{ + m_hidppInfo.clear(); + + if (hdc->busType() == BusType::Usb) { + m_hidppInfo.receiverState = toString(hdc->receiverState(), false); + } + + m_hidppInfo.presenterState = toString(hdc->presenterState(), false); + + const auto pv = hdc->protocolVersion(); + m_hidppInfo.protocolVersion = QString("%1.%2").arg(pv.major).arg(pv.minor); + + for (const auto flag : { DeviceFlag::Vibrate + , DeviceFlag::ReportBattery + , DeviceFlag::NextHold + , DeviceFlag::BackHold + , DeviceFlag::PointerSpeed }) + { + if (hdc->hasFlags(flag)) m_hidppInfo.hidppFlags.push_back(toString(flag, false)); + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateBatteryInfo(SubHidppConnection* hdc) +{ + const auto batteryInfo = hdc->batteryInfo(); + if (batteryInfo.status == HIDPP::BatteryStatus::Discharging) + { + m_batteryInfo = QString("%1% - %2% (%3)").arg( + QString::number(batteryInfo.currentLevel), + QString::number(batteryInfo.nextReportedLevel), + toString(batteryInfo.status)); + } else { + m_batteryInfo = toString(batteryInfo.status); + } } diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 40ffbf3c..7aec6549 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -7,15 +7,21 @@ #include #include +#include +#include +#include + class DeviceConnection; class InputMapper; class MultiTimerWidget; class QComboBox; class QTabWidget; +class QTextEdit; class Settings; class Spotlight; class VibrationSettingsWidget; class SubDeviceConnection; +class SubHidppConnection; class TimerTabWidget; // ------------------------------------------------------------------------------------------------- @@ -26,7 +32,6 @@ class DevicesWidget : public QWidget public: explicit DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent = nullptr); const DeviceId currentDeviceId() const; - void updateDeviceDetails(Spotlight* spotlight); signals: void currentDeviceChanged(const DeviceId&); @@ -46,9 +51,6 @@ class DevicesWidget : public QWidget QPointer m_timerTabContext; QWidget* m_deviceDetailsTabWidget = nullptr; - // TODO Put into separate DeviceDetailsWidget - // QTextEdit* m_deviceDetailsTextEdit = nullptr; - QPointer m_inputMapper; }; @@ -81,5 +83,48 @@ class DeviceInfoWidget : public QWidget void setDeviceConnection(DeviceConnection* connection); private: + void initSubdeviceInfo(); + void updateSubdeviceInfo(SubDeviceConnection* sdc); + void connectToSubdeviceUpdates(SubDeviceConnection* sdc); + void connectToBatteryUpdates(SubHidppConnection* hdc); + void updateHidppInfo(SubHidppConnection* hdc); + void updateBatteryInfo(SubHidppConnection* hdc); + + void delayedTextEditUpdate(); + void updateTextEdit(); + + QTextEdit* m_textEdit = nullptr; + QTimer* m_delayedUpdateTimer = nullptr; + QTimer* m_batteryInfoTimer = nullptr; + + std::vector> m_deviceBaseInfo; + + struct SubDeviceInfo { + QString info; + bool isHidpp = false; + bool hasBatteryInfo = false; + }; + + std::map m_subDevices; + QString m_batteryInfo; + + struct HidppInfo { + QString receiverState; + QString presenterState; + QString protocolVersion; + QStringList hidppFlags; + + void clear() + { + receiverState.clear(); + presenterState.clear(); + protocolVersion.clear(); + hidppFlags.clear(); + } + }; + + HidppInfo m_hidppInfo; + + QPointer m_connectionContext; QPointer m_connection; }; \ No newline at end of file diff --git a/src/enum-helper.h b/src/enum-helper.h index 8c5ff3f1..b84881f7 100644 --- a/src/enum-helper.h +++ b/src/enum-helper.h @@ -39,3 +39,7 @@ constexpr auto to_enum(T v) { using PLURALNAME = ENUMCLASS; #define ENUM_CASE_STRINGIFY(x) case x: return #x +#define ENUM_CASE_STRINGIFY2(c, n) case c::n: return #n +#define ENUM_CASE_STRINGIFY3(c, n, b) case c::n: return b ? #c"::"#n : #n + +#define ENUM_STRINGIFY3(c, n, b) (b ? #c"::"#n : #n) diff --git a/src/hidpp.cc b/src/hidpp.cc index 3b5fdcc9..35914c16 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -696,6 +696,7 @@ const char* toString(HIDPP::BatteryStatus bs) ENUM_CASE_STRINGIFY(BatteryStatus::InvalidBattery); ENUM_CASE_STRINGIFY(BatteryStatus::SlowCharging); ENUM_CASE_STRINGIFY(BatteryStatus::ThermalError); + ENUM_CASE_STRINGIFY(BatteryStatus::Uninitialized); }; return "BatteryStatus::(unknown)"; } diff --git a/src/hidpp.h b/src/hidpp.h index 2686ca15..0dfc4ce6 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -90,7 +90,9 @@ namespace HIDPP { SlowCharging = 0x04, InvalidBattery = 0x05, ThermalError = 0x06, - ChargingError = 0x07 + ChargingError = 0x07, + + Uninitialized = 0xff // Custom value of Projecteur }; // ------------------------------------------------------------------------------------------------- @@ -98,7 +100,7 @@ namespace HIDPP { { uint8_t currentLevel = 0; uint8_t nextReportedLevel = 0; - BatteryStatus status = BatteryStatus::Discharging; + BatteryStatus status = BatteryStatus::Uninitialized; inline bool operator==(const BatteryInfo& rhs) const { return std::tie(currentLevel, nextReportedLevel, status) diff --git a/src/preferencesdlg.cc b/src/preferencesdlg.cc index cdf36259..28e301dc 100644 --- a/src/preferencesdlg.cc +++ b/src/preferencesdlg.cc @@ -98,11 +98,6 @@ PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, mainVBox->addWidget(tabWidget); mainVBox->addLayout(btnHBox); - // Update battery information when dialog is shown - connect(this, &PreferencesDialog::dialogActiveChanged, this, - [this, spotlight](bool active){ - if (active) m_deviceswidget->updateDeviceDetails(spotlight);}); - connect(overlayCheckBox, &QCheckBox::toggled, this, [settings](bool checked){ settings->setOverlayDisabled(!checked); }); diff --git a/src/spotlight.cc b/src/spotlight.cc index 646a3776..cfab7459 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -80,6 +81,7 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) , m_options(std::move(options)) , m_activeTimer(new QTimer(this)) , m_connectionTimer(new QTimer(this)) + , m_holdMoveEventTimer(new QTimer(this)) , m_settings(settings) , m_holdButtonStatus(std::make_unique()) { @@ -109,6 +111,9 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) connectDevices(); }); + m_holdMoveEventTimer->setSingleShot(true); + m_holdMoveEventTimer->setInterval(30); + // Try to find already attached device(s) and connect to it. connectDevices(); setupDevEventInotify(); @@ -223,8 +228,23 @@ int Spotlight::connectDevices() return hidppCon; } } - else { - return SubHidrawConnection::create(scanSubDevice, *dc); + else if (auto hidrawConn = SubHidrawConnection::create(scanSubDevice, *dc)) + { + QPointer connPtr(hidrawConn.get()); + // Remove device on socketReadError + connect(&*hidrawConn, &SubHidrawConnection::socketReadError, this, [this, connPtr](){ + if (!connPtr) return; + const bool anyConnectedBefore = anySpotlightDeviceConnected(); + connPtr->disconnect(); + QTimer::singleShot(0, this, [this, devicePath=connPtr->path(), anyConnectedBefore](){ + removeDeviceConnection(devicePath); + if (!anySpotlightDeviceConnected() && anyConnectedBefore) { + emit anySpotlightDeviceConnectedChanged(false); + } + }); + }); + + return hidrawConn; } } return std::shared_ptr(); @@ -468,44 +488,51 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) connection->registerNotificationCallback(this, rcIndex, makeSafeCallback([this, connection](Message&& msg) { + // Block some of the move events + // TODO This works quiet okay in combination with adjusting x and y values, + // but needs to be a more solid option to accumulate the mass of move events + // and consolidate them to a number of meaningful action special key events. + if (m_holdMoveEventTimer->isActive()) return; + m_holdMoveEventTimer->start(); + // byte 4 : -1 for left movement, 0 for right movement // byte 5 : horizontal movement speed -128 to 127 // byte 6 : -1 for up movement, 0 for down movement // byte 7 : vertical movement speed -128 to 127 static const auto intcast = [](uint8_t v) -> int{ return static_cast(v); }; - // logDebug(hid) << tr("4 = %1, 5 = %2, 6 = %3, 7 = %4") - // .arg(intcast(msg[4]), 4) - // .arg(intcast(msg[5]), 4) - // .arg(intcast(msg[6]), 4) - // .arg(intcast(msg[7]), 4); const int x = intcast(msg[5]); const int y = intcast(msg[7]); - static const auto getReducedParam = [](int param, int limit=2){ // reduce the values from Spotlight device for better scroll behavior - constexpr int minVal = 5; - if (abs(param) < minVal) return 0; // ignore small device movement - - const auto sign = (param == 0) ? 0 : ((param > 0) ? 1 :-1); - return ((abs(param) > minVal*limit)? sign*minVal*limit : param)/minVal; // limit return value between -limit to limit + static const auto getReducedParam = [](int param) -> int{ + constexpr int divider = 5; + constexpr int minimum = 5; + constexpr int maximum = 10; + if (std::abs(param) < minimum) return 0; + const auto sign = (param == 0) ? 0 : ((param > 0) ? 1 : -1); + return std::floor(1.0 * ((abs(param) > maximum)? sign * maximum : param) / divider); }; + const int adjustedX = getReducedParam(x); + const int adjustedY = getReducedParam(y); + + if (adjustedX == 0 && adjustedY == 0) return; + static const auto scrollHAction = GlobalActions::scrollHorizontal(); - scrollHAction->param = -(getReducedParam(x)); + scrollHAction->param = -adjustedX; static const auto scrollVAction = GlobalActions::scrollVertical(); - scrollVAction->param = getReducedParam(y); + scrollVAction->param = adjustedY; static const auto volumeControlAction = GlobalActions::volumeControl(); - volumeControlAction->param = -getReducedParam(y, 3); + volumeControlAction->param = -adjustedY; if (!connection->inputMapper()->recordingMode()) { - // feed the keystroke to InputMapper and let it trigger the associated action - for (auto key_event : m_holdButtonStatus->moveKeyEventSeq()) { - connection->inputMapper()->addEvents(key_event); - } + for (auto key_event : m_holdButtonStatus->moveKeyEventSeq()) { + connection->inputMapper()->addEvents(key_event); + } } }), 1 /* function 1 */); } diff --git a/src/spotlight.h b/src/spotlight.h index 59e75479..ea04e15a 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -74,6 +74,7 @@ class Spotlight : public QObject, public async::Async QTimer* m_activeTimer = nullptr; QTimer* m_connectionTimer = nullptr; + QTimer* m_holdMoveEventTimer = nullptr; bool m_spotActive = false; std::shared_ptr m_virtualDevice; Settings* m_settings = nullptr; From 515a1d1c407d39eb9f96d54dc41bdc13a837344a Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sat, 18 Sep 2021 00:00:09 +0900 Subject: [PATCH 070/110] Updated docs for Projecteur v1.0 --- README.md | 32 ++++++++++++++++++++++++++------ doc/LogitechSpotlightHID++.md | 30 ++++++++++++++++-------------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ad4071ed..4b57b35a 100644 --- a/README.md +++ b/README.md @@ -98,16 +98,36 @@ line option - button mapping will be disabled then.) Input events from the presenter device can be mapped to different actions. The _Key Sequence_ action is particularly powerful as it can emit any user-defined keystroke. These keystrokes can invoke shortcut in presentation software -(or any other software) being used. +(or any other software) being used. Similarly, the _Cycle Preset_ action can be +used for cycling different spotlight presets. However, it should be noted that +presets might get reordered after program restart. If user want to maintain the +order of presets, please prepend the name of preset with number. For example, +in stead of naming `Pointer` and `Highlight`, name them `1.Pointer` and +`2.Highlight` to maintain the order. #### Hold Button Mapping for Logitech Spotlight Logitech Spotlight can send Hold event for Next and Back buttons as HID++ -messages. For mapping those inputs, please ensure that the device is active -by pressing any button, then go to Input Mapping tab under Devices tab in -Preferences dialog box and right click in first column (Input Sequence) for -any entry. Additional mapped actions (e.g. _Vertical Scrolling_ or _Volume control_) -can be selected for these special hold events. +messages. Using this device feature, this program provides three different +usage of the Next or Hold button. + + 1. Button Tap + 2. Long-Press Event + 3. Button Hold followed by device movement or Hold Move Event + +In Input Mapper tab (Devices tab in Preferences dialog box), the first two +button usage (_i.e._ tap and long-press) can be mapped directly by tapping or +long pressing the relevant button. For mapping the third button usage (_i.e._ +Hold Move Event), please ensure that the device is active by pressing any button, +and then right click in first column (Input Sequence) for any entry and select +the relevant option. Additional mapped actions (e.g. _Vertical Scrolling_, +_Horizontal Scrolling_, or _Volume control_) can be selected for these hold +move events. + +Please note that in case when both Long-Press event and Hold Move events are +mapped for a particular button, both actions will executed if user hold the +button and move device. To avoid this situation, do not set both Long-Press +and Hold Move actions for the same button. ## Download diff --git a/doc/LogitechSpotlightHID++.md b/doc/LogitechSpotlightHID++.md index 40b65b3c..897190aa 100644 --- a/doc/LogitechSpotlightHID++.md +++ b/doc/LogitechSpotlightHID++.md @@ -144,7 +144,7 @@ The FeatureSet table for a device may change with a firmware update. The application should cache FeatureSet table along with Firmware version and only read FeatureSet table again if the firmware version has changed. This logic for getting FeatureSet table from device is implemented in -`populateFeatureTable` method in `FeatureSet` class in [hidpp.h](../src/hidpp.h). +`initFromDevice` method in `FeatureSet` class in [hidpp.h](../src/hidpp.h). # Resetting Logitech Spotlight device @@ -160,7 +160,7 @@ Spotlight device can be reset with following HID++ message from the application: ``` 2. Load the FeatureSet table for the device (from pre-existing cache or from - the device if firmware version has changed by calling `populateFeatureTable` + the device if firmware version has changed by calling `initFromDevice` method in `FeatureSet` class in [hidpp.h](../src/hidpp.h)). 3. Reset the Spotlight device with the Feature index for Reset Feature Code @@ -188,14 +188,12 @@ following HID++ commands: {0x10, 0x01, 0x0a, 0x1d, 0x14, 0x00, 0x00} ``` -These initialization steps are implemented in `initialize` method of -`SubHidppConnection` class in [device.h](../src/device.h). After reprogramming -the Next and Back buttons, the first and second response of the spotlight -device contains relative mouse move data through HID++ messages -(not regular input events to the OS). These events are handled in `onHidppDataAvailable` -method in `Spotlight` class in [spotlight.h](../src/spotlight.h). Special -'repeated' actions (like scrolling and volume control) can utilize the relative -mouse movement data received in the responses. +These initialization steps are implemented in `initReceiver` and `initPresenter` +methods of `SubHidppConnection` class in [device-hidpp.h](../src/device-hidpp.h). +After reprogramming the Next and Back buttons, the spotlight device will send +mouse movement data when either of these button are long-pressed and device is +moved. The processing of these events are discussed in the +[following section](#response-to-`next-hold`-and-`back-hold`-keys). For completeness, it should be noted that the official Logitech Spotlight software reprogram the click and double click events too by following HID++ @@ -265,10 +263,10 @@ All of the HID++ commands listed above result in response messages from the Spotlight device. For most messages, these responses from device are just the acknowledgements of the HID++ commands sent by the application. However, some responses from the Spotlight device contain useful information. These responses -are processed in the `onHidppDataAvailable` method in the `Spotlight` class -in [spotlight.h](../src/spotlight.h). Description of HID++ messages from device -to reprogrammed keys (`Next Hold` and `Back Hold`) are provided in following -sub-section: +are processed in the `onHidppDataAvailable` method in the `SubHidppConnection` +class in [device-hidpp.h](../src/device-hidpp.h). Description of HID++ messages +from device to reprogrammed keys (`Next Hold` and `Back Hold`) are provided in +following sub-section: ## Response to `Next Hold` and `Back Hold` keys @@ -287,6 +285,10 @@ In the four bytes (for mouse data), the second and last bytes are relative `x` and `y` values. These relative `x` and `y` values are used for Scrolling and Volume Control Actions in Projecteur. +The relevant functions for processing `Next Hold` and `Back Hold` are provided +in `registerForNotifications` method in the `Spotlight` class +([spotlight.h](../src/spotlight.h)). + # Further information For more information about HID++ protocol in general please check From 95ac1f7c7d536d32420c9d8a69953b57970dcd49 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 17 Sep 2021 17:12:28 +0200 Subject: [PATCH 071/110] Clean up TODOs and comments, log closing of file descriptors. --- src/device-hidpp.cc | 8 ++------ src/device-vibration.cc | 7 +++++-- src/device.cc | 33 +++++++++++++++++++++++---------- src/device.h | 2 +- src/deviceinput.cc | 27 ++++++++++++++++++++++----- src/deviceinput.h | 3 +++ src/inputmapconfig.cc | 3 ++- src/inputseqedit.cc | 4 ++-- src/spotlight.cc | 7 ------- src/virtualdevice.cc | 2 +- 10 files changed, 61 insertions(+), 35 deletions(-) diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index a0e77ed2..c7638abc 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -349,7 +349,7 @@ std::shared_ptr SubHidppConnection::create(const DeviceScan: auto connection = std::make_shared(Token{}, dc.deviceId(), sd); if (dc.hasHidppSupport()) connection->m_details.deviceFlags |= DeviceFlag::Hidpp; - connection->createSocketNotifiers(devfd); + connection->createSocketNotifiers(devfd, sd.deviceFile); connection->m_inputMapper = dc.inputMapper(); connect(connection->socketReadNotifier(), &QSocketNotifier::activated, &*connection, @@ -371,10 +371,7 @@ void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length, return; } - // TODO generalize features and protocol for proprietary device features like vibration - // for not only the Spotlight device. - // - // Spotlight: + // Logitech Spotlight: // present // controlID len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80}; @@ -625,7 +622,6 @@ void SubHidppConnection::initFeatures( RequestBatch batch; auto resultMap = std::make_shared(); - // TODO: Is resetting the device necessary? // Reset spotlight device, if supported if (const auto resetFeatureIndex = m_featureSet.featureIndex(FeatureCode::Reset)) { diff --git a/src/device-vibration.cc b/src/device-vibration.cc index fc39b17c..ea75da48 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -22,6 +22,8 @@ #include #include +DECLARE_LOGGING_CATEGORY(hid) + // ------------------------------------------------------------------------------------------------- namespace { constexpr int numTimers = 3; @@ -420,7 +422,8 @@ void VibrationSettingsWidget::sendVibrateCommand() const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); m_subDeviceConnection->sendVibrateCommand(vint, vlen, - [](HidppConnectionInterface::MsgResult /* result */, HIDPP::Message&& /* msg */) { - // TODO debug log vibrate command reply? + [](HidppConnectionInterface::MsgResult result, HIDPP::Message&& msg) { + logDebug(hid) << tr("Vibrate command returned: %1 (%2)") + .arg(toString(result)).arg(msg.hex()); }); } diff --git a/src/device.cc b/src/device.cc index eb9cfad1..56e39d47 100644 --- a/src/device.cc +++ b/src/device.cc @@ -263,11 +263,13 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: connection->m_readNotifier = std::make_unique(evfd, QSocketNotifier::Read); QSocketNotifier* const notifier = connection->m_readNotifier.get(); // Auto clean up and close descriptor on destruction of notifier - connect(notifier, &QSocketNotifier::destroyed, [grabbed = connection->m_details.grabbed, notifier]() { + connect(notifier, &QSocketNotifier::destroyed, + [grabbed = connection->m_details.grabbed, evfd, path=sd.deviceFile]() { if (grabbed) { - ioctl(static_cast(notifier->socket()), EVIOCGRAB, 0); + ioctl(evfd, EVIOCGRAB, 0); } - ::close(static_cast(notifier->socket())); + logDebug(device) << tr("Closing file descriptor for '%1'").arg(path); + ::close(evfd); }); connection->m_inputMapper = dc.inputMapper(); @@ -303,7 +305,7 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca if (devfd == -1) return std::shared_ptr(); auto connection = std::make_shared(Token{}, dc.deviceId(), sd); - connection->createSocketNotifiers(devfd); + connection->createSocketNotifiers(devfd, sd.deviceFile); connect(connection->socketReadNotifier(), &QSocketNotifier::activated, &*connection, &SubHidrawConnection::onHidrawDataAvailable); @@ -376,7 +378,6 @@ ssize_t SubHidrawConnection::sendData(const void* msg, size_t msgLen) constexpr ssize_t errorResult = -1; if (mode() != ConnectionMode::ReadWrite || !m_writeNotifier) { return errorResult; } - // TODO check against m_writeNotifier? const auto res = ::write(m_writeNotifier->socket(), msg, msgLen); if (static_cast(res) == msgLen) { @@ -390,7 +391,7 @@ ssize_t SubHidrawConnection::sendData(const void* msg, size_t msgLen) } // ------------------------------------------------------------------------------------------------- -void SubHidrawConnection::createSocketNotifiers(int fd) +void SubHidrawConnection::createSocketNotifiers(int fd, const QString& path) { fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(fd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { @@ -400,17 +401,29 @@ void SubHidrawConnection::createSocketNotifiers(int fd) // Create read and write socket notifiers m_readNotifier = std::make_unique(fd, QSocketNotifier::Read); QSocketNotifier *const readNotifier = m_readNotifier.get(); + auto fdPtr = std::make_shared(fd); + // Auto clean up and close descriptor on destruction of notifier - connect(readNotifier, &QSocketNotifier::destroyed, [readNotifier]() { - ::close(static_cast(readNotifier->socket())); + connect(readNotifier, &QSocketNotifier::destroyed, [fdPtr, path]() + { + if (fdPtr && *fdPtr != -1) { + logDebug(device) << tr("Closing file descriptor for '%1'").arg(path); + ::close(*fdPtr); + *fdPtr = -1; + } }); m_writeNotifier = std::make_unique(fd, QSocketNotifier::Write); QSocketNotifier *const writeNotifier = m_writeNotifier.get(); writeNotifier->setEnabled(false); // Disable write notifier by default // Auto clean up and close descriptor on destruction of notifier - connect(writeNotifier, &QSocketNotifier::destroyed, [writeNotifier]() { - ::close(static_cast(writeNotifier->socket())); + connect(writeNotifier, &QSocketNotifier::destroyed, [fdPtr, path]() + { + if (fdPtr && *fdPtr != -1) { + logDebug(device) << tr("Closing file descriptor for '%1'").arg(path); + ::close(*fdPtr); + *fdPtr = -1; + } }); } diff --git a/src/device.h b/src/device.h index d9a9a8c5..1c515859 100644 --- a/src/device.h +++ b/src/device.h @@ -194,7 +194,7 @@ class SubHidrawConnection : public SubDeviceConnection, public HidrawConnectionI ssize_t sendData(const void* msg, size_t msgLen) override; protected: - void createSocketNotifiers(int fd); + void createSocketNotifiers(int fd, const QString& path); static int openHidrawSubDevice(const DeviceScan::SubDevice& sd, const DeviceId& devId); std::unique_ptr m_writeNotifier; diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 329a67ea..15138d82 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -510,6 +510,22 @@ const NativeKeySequence& NativeKeySequence::predefined::meta() return ks; } + +// ------------------------------------------------------------------------------------------------- +const char* toString(Action::Type at, bool withClass) +{ + using Type = Action::Type; + switch (at) { + ENUM_CASE_STRINGIFY3(Type, KeySequence, withClass); + ENUM_CASE_STRINGIFY3(Type, CyclePresets, withClass); + ENUM_CASE_STRINGIFY3(Type, ToggleSpotlight, withClass); + ENUM_CASE_STRINGIFY3(Type, ScrollHorizontal, withClass); + ENUM_CASE_STRINGIFY3(Type, ScrollVertical, withClass); + ENUM_CASE_STRINGIFY3(Type, VolumeControl, withClass); + } + return withClass ? "Type::(unknown)" : "(unkown)"; +} + // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- struct InputMapper::Impl @@ -551,8 +567,8 @@ void InputMapper::Impl::execAction(const std::shared_ptr& action, Device { if (!action || action->empty()) return; - logDebug(input) << "Input map action, type = " << int(action->type()) - << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit); + logDebug(input) << "Input map execAction, type =" << toString(action->type()) + << ", partial_hit =" << (r == DeviceKeyMap::Result::PartialHit); if (action->type() == Action::Type::KeySequence) { @@ -830,10 +846,11 @@ namespace SpecialKeys // ------------------------------------------------------------------------------------------------- const std::map& keyEventSequenceMap() { - // TODO Make names translateable static const std::map keyMap { - {Key::BackHoldMove, {"Back Hold Move", makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove))}}, - {Key::NextHoldMove, {"Next Hold Move", makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove))}}, + {Key::BackHoldMove, {InputMapper::tr("Back Hold Move"), + makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove))}}, + {Key::NextHoldMove, {InputMapper::tr("Next Hold Move"), + makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove))}}, }; return keyMap; } diff --git a/src/deviceinput.h b/src/deviceinput.h index 82a52fa4..b1bbf6ba 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -183,6 +183,9 @@ struct Action virtual bool empty() const = 0; }; +// ------------------------------------------------------------------------------------------------- +const char* toString(Action::Type at, bool withClass = true); + // ------------------------------------------------------------------------------------------------- struct KeySequenceAction : public Action { diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index 4b32c9f1..c5a4c15b 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -196,7 +196,8 @@ void InputMapConfigModel::setKeySequence(const QModelIndex& index, const NativeK if (index.row() < static_cast(m_configItems.size())) { auto& c = m_configItems[index.row()]; - // TODO if action is currently not a keysequence action.. -> just change action type? + // If the current action is not a keysequence action + // -> setting the key sequence is currently ignored. if (auto action = std::dynamic_pointer_cast(c.action)) { if (action->keySequence != ks) { diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index d4ff0c1e..d13a6643 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -47,7 +47,7 @@ namespace { static auto const pressChar = QChar(0x2193); // ↓ static auto const releaseChar = QChar(0x2191); // ↑ - // TODO some devices (e.g. August WP 200) have buttons that send a key combination + // TODO Some devices (e.g. August WP 200) have buttons that send a key combination // (modifiers + key) - this is ignored completely right now. const auto text = QString("[%1%2%3") .arg(ke.back().code != SYN_REPORT ? ke.back().code : ke.front().code, 0, 16) @@ -507,7 +507,7 @@ QSize InputSeqDelegate::sizeHint(const QStyleOptionViewItem& option, { if (const auto imModel = qobject_cast(index.model())) { - // TODO calc size hint from KeyEventSequence..... + // TODO Calculate size hint from KeyEventSequence..... return QStyledItemDelegate::sizeHint(option, index); } return QStyledItemDelegate::sizeHint(option, index); diff --git a/src/spotlight.cc b/src/spotlight.cc index cfab7459..d2f37e83 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -267,8 +267,6 @@ int Spotlight::connectDevices() connect(im, &InputMapper::actionMapped, this, [this](std::shared_ptr action) { - // TODO allow hold button move events only with specific actions - if (action->type() == Action::Type::CyclePresets) { auto it = std::find(m_settings->presets().cbegin(), m_settings->presets().cend(), lastPreset); @@ -469,11 +467,6 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; - if (!m_holdButtonStatus->nextPressed() && isNextPressed - && !m_holdButtonStatus->backPressed() && isBackPressed) { - // TODO KeyEvnt with both presses at the same time - } - if (!m_holdButtonStatus->nextPressed() && isNextPressed) { connection->inputMapper()->addEvents(KeyEvent{{EV_KEY, to_integral(HoldButtonStatus::Button::Next), 1}}); } diff --git a/src/virtualdevice.cc b/src/virtualdevice.cc index 6e7b9ea1..80809137 100644 --- a/src/virtualdevice.cc +++ b/src/virtualdevice.cc @@ -62,7 +62,7 @@ std::shared_ptr VirtualDevice::create(const char* name, uinp.id.version = virtualVersionId; // Setup the uinput device - // TODO Are the following Key and Event bits sufficient? Do we need more? (see all in Linux's input-event-codes.h) + // (see all in Linux's input-event-codes.h) ioctl(fd, UI_SET_EVBIT, EV_SYN); ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_EVBIT, EV_REL); From 4e185e47fec39a8407b6eb362daabc0e584cca62 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 18 Sep 2021 10:31:15 +0200 Subject: [PATCH 072/110] Fix clang-tidy warnings where feasible (pt1) + add initial clang-tidy config file + check device-hidpp, hidpp & aboutdlg --- .clang-tidy | 27 ++++++ CMakeLists.txt | 2 +- cmake/modules/GitVersion.cc.in | 2 +- cmake/modules/GitVersion.h.in | 2 +- src/aboutdlg.cc | 13 +-- src/device-hidpp.cc | 145 +++++++++++++++++---------------- src/devicescan.h | 23 ------ src/hidpp.cc | 103 +++++++++++------------ src/hidpp.h | 4 +- 9 files changed, 167 insertions(+), 154 deletions(-) create mode 100644 .clang-tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..366832e2 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,27 @@ +--- +Checks: > + *,-fuchsia*,-android-*, + -modernize-pass-by-value,-modernize-use-trailing-return-type, + -llvmlibc-restrict-system-libc-headers,-llvmlibc-*, + -altera-unroll-loops,-altera-struct-pack-align, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay,-hicpp-no-array-decay, + -llvm-qualified-auto,-readability-qualified-auto, + -cppcoreguidelines-avoid-magic-numbers, + -google-build-using-namespace, + -cppcoreguidelines-pro-type-vararg,-hicpp-vararg, + -cppcoreguidelines-pro-type-static-cast-downcast, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -readability-implicit-bool-conversion, + -hicpp-signed-bitwise, + -cppcoreguidelines-avoid-c-arrays,-hicpp-avoid-c-arrays,-modernize-avoid-c-arrays, + -google-default-arguments + +WarningsAsErrors: false +CheckOptions: + - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: true + - key: readability-magic-numbers.IgnorePowersOf2IntegerValues + value: true + - key: readability-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;5;6;10;24;60;100;1000;' diff --git a/CMakeLists.txt b/CMakeLists.txt index eb5aeaf2..c2d5177e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeR project(Projecteur LANGUAGES CXX) add_compile_options(-Wall -Wextra -Werror) -#set(CMAKE_CXX_CLANG_TIDY "clang-tidy-9;-checks=*,-fuchsia*,-modernize-pass-by-value") +#set(CMAKE_CXX_CLANG_TIDY clang-tidy-12) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") include(GitVersion) diff --git a/cmake/modules/GitVersion.cc.in b/cmake/modules/GitVersion.cc.in index 561555d0..d2f4e557 100644 --- a/cmake/modules/GitVersion.cc.in +++ b/cmake/modules/GitVersion.cc.in @@ -12,4 +12,4 @@ namespace @TARGET@ { bool version_isdirty() { return @VERSION_ISDIRTY@; } const char* version_branch() { return "@VERSION_BRANCH@"; } const char* version_buildtype() { return "@VERSION_BUILDTYPE@"; } -} +} // end namespace @TARGET@ diff --git a/cmake/modules/GitVersion.h.in b/cmake/modules/GitVersion.h.in index 25f693f2..400d25c4 100644 --- a/cmake/modules/GitVersion.h.in +++ b/cmake/modules/GitVersion.h.in @@ -12,4 +12,4 @@ namespace @TARGET@ { bool version_isdirty(); const char* version_branch(); const char* version_buildtype(); -} +} // end namespace @TARGET@ diff --git a/src/aboutdlg.cc b/src/aboutdlg.cc index 52497d24..12736671 100644 --- a/src/aboutdlg.cc +++ b/src/aboutdlg.cc @@ -233,18 +233,21 @@ QWidget* AboutDialog::createThirdPartyLicensesWidget() for (const auto& tpl : thirdPartyProjects) { html += "

  • "; - if (tpl.projectUrl.size()) + if (tpl.projectUrl.size()) { html += QString("%2").arg(tpl.projectUrl, tpl.projectName); - else + } else { html += QString("%1").arg(tpl.projectName); + } - if (tpl.copyrightNotice.size()) + if (tpl.copyrightNotice.size()) { html += "
    " + tpl.copyrightNotice + ""; + } - if (tpl.licenseUrl.size()) + if (tpl.licenseUrl.size()) { html += QString("
    %2").arg(tpl.licenseUrl, tpl.licenseName); - else + } else { html += QString("
    License: %1").arg(tpl.licenseName); + } html += "
  • "; } diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index c7638abc..472bf860 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -2,10 +2,10 @@ // - See LICENSE.md and README.md #include "device-hidpp.h" -#include "logging.h" -#include "enum-helper.h" #include "deviceinput.h" +#include "enum-helper.h" +#include "logging.h" #include @@ -21,7 +21,8 @@ SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token, , m_featureSet(this) , m_requestCleanupTimer(new QTimer(this)) { - m_requestCleanupTimer->setInterval(500); + constexpr int cleanUpTimerInterval = 500; + m_requestCleanupTimer->setInterval(cleanUpTimerInterval); m_requestCleanupTimer->setSingleShot(false); connect(m_requestCleanupTimer, &QTimer::timeout, this, &SubHidppConnection::clearTimedOutRequests); } @@ -103,7 +104,7 @@ void SubHidppConnection::sendData(HIDPP::Message msg, SendResultCallback resultC postSelf([this, msg = std::move(msg), cb = std::move(resultCb)]() mutable { // Check for valid message format if (!msg.isValid()) { - if (cb) cb(MsgResult::InvalidFormat); + if (cb) { cb(MsgResult::InvalidFormat); } return; } @@ -132,7 +133,7 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r { // Check for valid message format if (!msg.isValid()) { - if (cb) cb(MsgResult::InvalidFormat, HIDPP::Message()); + if (cb) { cb(MsgResult::InvalidFormat, HIDPP::Message()); } return; } @@ -150,7 +151,7 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r { logWarn(hid) << tr("Invalid device index (%1) in message for '%2'") .arg(msg.deviceIndex()).arg(path()); - if (cb) cb(MsgResult::InvalidFormat, HIDPP::Message()); + if (cb) { cb(MsgResult::InvalidFormat, HIDPP::Message()); } return; } @@ -178,13 +179,15 @@ void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback r m_requests.erase(it); })); + constexpr uint64_t hidppMsgTimeoutMs = 4000; + // Place request in request list with a timeout m_requests.emplace_back(RequestEntry{ - std::move(msg), std::chrono::steady_clock::now() + std::chrono::milliseconds{4000}, + std::move(msg), std::chrono::steady_clock::now() + std::chrono::milliseconds{hidppMsgTimeoutMs}, std::move(cb)}); // Run cleanup timer if not already active - if (!m_requestCleanupTimer->isActive()) m_requestCleanupTimer->start(); + if (!m_requestCleanupTimer->isActive()) { m_requestCleanupTimer->start(); } }); } @@ -200,11 +203,11 @@ void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallb void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, bool continueOnError, std::vector results) { - postSelf([this, batch = std::move(dataBatch), batchCb = std::move(cb), continueOnError, + postSelf([this, batch = std::move(dataBatch), batchCb = std::move(cb), results = std::move(results), coe = continueOnError]() mutable { if (batch.empty()) { - if (batchCb) batchCb(std::move(results)); + if (batchCb) { batchCb(std::move(results)); } return; } @@ -221,12 +224,12 @@ void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallb // Add result to results vector results.push_back(result); // If a result callback is set invoke it - if (resultCb) resultCb(result); + if (resultCb) { resultCb(result); } // If batch is empty or we got an error result and don't want to continue on // error (coe) if (batch.empty() || (result != MsgResult::Ok && !coe)) { - if (batchCb) batchCb(std::move(results)); + if (batchCb) { batchCb(std::move(results)); } return; } @@ -248,11 +251,11 @@ void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatc void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, bool continueOnError, std::vector results) { - postSelf([this, batch = std::move(requestBatch), batchCb = std::move(cb), continueOnError, + postSelf([this, batch = std::move(requestBatch), batchCb = std::move(cb), results = std::move(results), coe = continueOnError]() mutable { if (batch.empty()) { - if (batchCb) batchCb(std::move(results)); + if (batchCb) { batchCb(std::move(results)); } return; } @@ -269,12 +272,12 @@ void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatc // Add result to results vector results.push_back(result); // If a result callback is set invoke it - if (resultCb) resultCb(result, std::move(replyMessage)); + if (resultCb) { resultCb(result, std::move(replyMessage)); } // If batch is empty or we got an error result and don't want to continue on // error (coe) if (batch.empty() || (result != MsgResult::Ok && !coe)) { - if (batchCb) batchCb(std::move(results)); + if (batchCb) { batchCb(std::move(results)); } return; } @@ -288,7 +291,7 @@ void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatc void SubHidppConnection::registerNotificationCallback(QObject* obj, uint8_t featureIndex, NotificationCallback cb, uint8_t function) { - if (obj == nullptr || !cb) return; + if (obj == nullptr || !cb) { return; } postSelf([this, obj, featureIndex, function, cb=std::move(cb)]() { @@ -324,8 +327,7 @@ void SubHidppConnection::unregisterNotificationCallback(QObject* obj, auto& callbackList = m_notificationSubscribers[featureIndex]; callbackList.remove_if([obj, function](const Subscriber& item){ if (item.object == obj) { - if (function > 15) return true; - if (item.function == function) return true; + if (function > 15 || item.function == function) { return true; } } return false; }); @@ -344,10 +346,10 @@ void SubHidppConnection::unregisterNotificationCallback(QObject* obj, std::shared_ptr SubHidppConnection::create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc) { const int devfd = openHidrawSubDevice(sd, dc.deviceId()); - if (devfd == -1) return std::shared_ptr(); + if (devfd == -1) { return std::shared_ptr(); } auto connection = std::make_shared(Token{}, dc.deviceId(), sd); - if (dc.hasHidppSupport()) connection->m_details.deviceFlags |= DeviceFlag::Hidpp; + if (dc.hasHidppSupport()) { connection->m_details.deviceFlags |= DeviceFlag::Hidpp; } connection->createSocketNotifiers(devfd, sd.deviceFile); connection->m_inputMapper = dc.inputMapper(); @@ -367,7 +369,7 @@ void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length, if (pcIndex == 0) { - if (cb) cb(MsgResult::FeatureNotSupported, HIDPP::Message()); + if (cb) { cb(MsgResult::FeatureNotSupported, HIDPP::Message()); } return; } @@ -396,14 +398,14 @@ void SubHidppConnection::getBatteryLevelStatus( const auto batteryIndex = m_featureSet.featureIndex(FeatureCode::BatteryStatus); if (batteryIndex == 0) { - if (cb) cb(MsgResult::FeatureNotSupported, {}); + if (cb) { cb(MsgResult::FeatureNotSupported, {}); } return; } Message batteryReqMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, batteryIndex, 0); - sendRequest(std::move(batteryReqMsg), [cb=std::move(cb)](MsgResult res, Message&& msg) + sendRequest(std::move(batteryReqMsg), [cb=std::move(cb)](MsgResult res, Message&& msg) mutable { - if (!cb) return; + if (!cb) { return; } auto batteryInfo = (res != MsgResult::Ok) ? BatteryInfo{} : BatteryInfo{msg[4], @@ -420,7 +422,7 @@ void SubHidppConnection::setPointerSpeed(uint8_t speed, const uint8_t psIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PointerSpeed); if (psIndex == 0x00) { - if (cb) cb(MsgResult::FeatureNotSupported, HIDPP::Message()); + if (cb) { cb(MsgResult::FeatureNotSupported, HIDPP::Message()); } return; } @@ -438,7 +440,7 @@ void SubHidppConnection::setPointerSpeed(uint8_t speed, // ------------------------------------------------------------------------------------------------- void SubHidppConnection::setReceiverState(ReceiverState rs) { - if (rs == m_receiverState) return; + if (rs == m_receiverState) { return; } logDebug(hid) << tr("Receiver state (%1) changes from %3 to %4") .arg(path()).arg(toString(m_receiverState), toString(rs)); @@ -449,7 +451,7 @@ void SubHidppConnection::setReceiverState(ReceiverState rs) // ------------------------------------------------------------------------------------------------- void SubHidppConnection::setPresenterState(PresenterState ps) { - if (ps == m_presenterState) return; + if (ps == m_presenterState) { return; } logDebug(hid) << tr("Presenter state (%1) changes from %2 to %3") .arg(path()).arg(toString(m_presenterState), toString(ps)); @@ -460,7 +462,7 @@ void SubHidppConnection::setPresenterState(PresenterState ps) // ------------------------------------------------------------------------------------------------- void SubHidppConnection::setBatteryInfo(const HIDPP::BatteryInfo& bi) { - if (m_batteryInfo == bi) return; + if (m_batteryInfo == bi) { return; } m_batteryInfo = bi; emit batteryInfoChanged(m_batteryInfo); @@ -469,12 +471,12 @@ void SubHidppConnection::setBatteryInfo(const HIDPP::BatteryInfo& bi) // ------------------------------------------------------------------------------------------------- void SubHidppConnection::initReceiver(std::function cb) { - postSelf([this, cb=std::move(cb)](){ + postSelf([this, cb=std::move(cb)]() mutable { if (m_receiverState == ReceiverState::Initializing || m_receiverState == ReceiverState::Initialized) { logDebug(hid) << "Cannot init receiver when initializing or already initialized."; - if (cb) cb(m_receiverState); + if (cb) { cb(m_receiverState); } return; } @@ -484,7 +486,7 @@ void SubHidppConnection::initReceiver(std::function cb) { // If bus type is not USB return immediately with success result and initialized state setReceiverState(ReceiverState::Initialized); - if (cb) cb(m_receiverState); + if (cb) { cb(m_receiverState); } return; } @@ -497,8 +499,8 @@ void SubHidppConnection::initReceiver(std::function cb) RequestBatchItem{ // Reset device: get rid of any device configuration by other programs Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 0, {}), - [index=++index](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; + [index=++index](MsgResult result, HIDPP::Message&& /* msg */) { + if (result == MsgResult::Ok) { return; } logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); } @@ -507,8 +509,8 @@ void SubHidppConnection::initReceiver(std::function cb) // Turn off software bit and keep the wireless notification bit on Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x01, 0x00}), - [index=++index](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; + [index=++index](MsgResult result, HIDPP::Message&& /* msg */) { + if (result == MsgResult::Ok) { return; } logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); } @@ -516,8 +518,8 @@ void SubHidppConnection::initReceiver(std::function cb) RequestBatchItem{ // Initialize USB dongle Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 2, {}), - [index=++index](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; + [index=++index](MsgResult result, HIDPP::Message&& /* msg */) { + if (result == MsgResult::Ok) { return; } logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); } @@ -526,8 +528,8 @@ void SubHidppConnection::initReceiver(std::function cb) // --- Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 2, {0x02, 0x00, 0x00}), - [index=++index](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; + [index=++index](MsgResult result, HIDPP::Message&& /* msg */) { + if (result == MsgResult::Ok) { return; } logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); } @@ -536,8 +538,8 @@ void SubHidppConnection::initReceiver(std::function cb) // Now enable both software and wireless notification bit Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0, {0x00, 0x09, 0x00}), - [index=++index](MsgResult result, HIDPP::Message /* msg */) { - if (result == MsgResult::Ok) return; + [index=++index](MsgResult result, HIDPP::Message&& /* msg */) { + if (result == MsgResult::Ok) { return; } logWarn(hid) << tr("Usb receiver init error; step %1: %2") .arg(index).arg(toString(result)); } @@ -549,7 +551,7 @@ void SubHidppConnection::initReceiver(std::function cb) { setReceiverState(results.back() == MsgResult::Ok ? ReceiverState::Initialized : ReceiverState::Error); - if (cb) cb(m_receiverState); + if (cb) { cb(m_receiverState); } }, false)); }); } @@ -557,20 +559,20 @@ void SubHidppConnection::initReceiver(std::function cb) // ------------------------------------------------------------------------------------------------- void SubHidppConnection::initPresenter(std::function cb) { - postSelf([this, cb=std::move(cb)](){ + postSelf([this, cb=std::move(cb)]() mutable { if (m_presenterState == PresenterState::Initializing || m_presenterState == PresenterState::Initialized_Offline || m_presenterState == PresenterState::Initialized_Online) { logDebug(hid) << "Cannot init presenter when offline, initializing or already initialized."; - if (cb) cb(m_presenterState); + if (cb) { cb(m_presenterState); } return; } setPresenterState(PresenterState::Initializing); m_featureSet.initFromDevice(makeSafeCallback( - [this, cb=std::move(cb)](HIDPP::FeatureSet::State state) + [this, cb=std::move(cb)](HIDPP::FeatureSet::State state) mutable { using FState = HIDPP::FeatureSet::State; switch (state) @@ -595,19 +597,19 @@ void SubHidppConnection::initPresenter(std::function cb) initFeatures(makeSafeCallback( [this, cb=std::move(cb)](std::map&& resultMap) { - if (resultMap.size()) { + if (!resultMap.empty()) { for (const auto& res : resultMap) { logDebug(hid) << tr("InitFeature result %1 => %2").arg(toString(res.first)).arg(toString(res.second)); } } emit featureSetInitialized(); setPresenterState(PresenterState::Initialized_Online); - if (cb) cb(m_presenterState); + if (cb) { cb(m_presenterState); } })); return; } } - if (cb) cb(m_presenterState); + if (cb) { cb(m_presenterState); } })); }); } @@ -627,7 +629,7 @@ void SubHidppConnection::initFeatures( { batch.emplace(RequestBatchItem { Message(Message::Type::Long, DeviceIndex::WirelessDevice1, resetFeatureIndex, 1), - [resultMap](MsgResult res, Message&&) { + [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::Reset, res); } }); @@ -641,7 +643,7 @@ void SubHidppConnection::initFeatures( batch.emplace(RequestBatchItem { Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3, Message::Data{0x00, 0xda, 0x33}), - [resultMap](MsgResult res, Message&&) { + [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::ReprogramControlsV4, res); } }); @@ -652,7 +654,7 @@ void SubHidppConnection::initFeatures( batch.emplace(RequestBatchItem { Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3, Message::Data{0x00, 0xdc, 0x33}), - [resultMap](MsgResult res, Message&&) { + [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::ReprogramControlsV4, res); } }); @@ -665,15 +667,15 @@ void SubHidppConnection::initFeatures( batch.emplace(RequestBatchItem { HIDPP::Message(HIDPP::Message::Type::Long, HIDPP::DeviceIndex::WirelessDevice1, psFeatureIndex, 1, HIDPP::Message::Data{0x14}), - [resultMap](MsgResult res, Message&&) { + [resultMap](MsgResult res, Message&& /* msg */) { resultMap->emplace(FeatureCode::PointerSpeed, res); } }); } sendRequestBatch(std::move(batch), - [resultMap=std::move(resultMap), cb=std::move(cb)](std::vector&&) { - if (cb) cb(std::move(*resultMap)); + [resultMap=std::move(resultMap), cb=std::move(cb)](std::vector&& /* msg */) mutable { + if (cb) { cb(std::move(*resultMap)); } }); } @@ -735,7 +737,7 @@ void SubHidppConnection::registerForFeatureNotifications() // Logitech button next and back press and hold + movement if (const auto rcIndex = m_featureSet.featureIndex(FeatureCode::ReprogramControlsV4)) { - registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) + registerNotificationCallback(this, rcIndex, makeSafeCallback([](Message&& msg) { // Logitech Spotlight: // * Next Button = 0xda @@ -796,7 +798,7 @@ void SubHidppConnection::registerForUsbNotifications() || m_presenterState == PresenterState::Error) { logInfo(hid) << tr("HID++ device '%1' came online.").arg(path()); - checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState /* ps */) { + checkAndUpdatePresenterState(makeSafeCallback([](PresenterState /* ps */) { //... })); } @@ -806,7 +808,7 @@ void SubHidppConnection::registerForUsbNotifications() // ------------------------------------------------------------------------------------------------- void SubHidppConnection::subDeviceInit() { - if (!hasFlags(DeviceFlag::Hidpp)) return; + if (!hasFlags(DeviceFlag::Hidpp)) { return; } registerForUsbNotifications(); @@ -816,7 +818,7 @@ void SubHidppConnection::subDeviceInit() Q_UNUSED(rs); // Independent of the receiver init result, try to initialize the // presenter device HID++ features and more - checkAndUpdatePresenterState(makeSafeCallback([this](PresenterState /* ps */) { + checkAndUpdatePresenterState(makeSafeCallback([](PresenterState /* ps */) { //... })); })); @@ -876,7 +878,7 @@ void SubHidppConnection::getProtocolVersion(std::function %1, version = %2.%3") .arg(toString(res)).arg(pv.major).arg(pv.minor); cb(res, (res == MsgResult::HidppError) ? msg.errorCode() - : HIDPP::Error::NoError, std::move(pv)); + : HIDPP::Error::NoError, pv); } }); } @@ -900,16 +902,16 @@ void SubHidppConnection::checkPresenterOnline(std::function cb) { - postSelf([this, cb=std::move(cb)]() + postSelf([this, cb=std::move(cb)]() mutable { if (m_presenterState == PresenterState::Initializing) { - if (cb) cb(m_presenterState); + if (cb) { cb(m_presenterState); } return; } checkPresenterOnline(makeSafeCallback( - [this, cb=std::move(cb)](bool isOnline, HIDPP::ProtocolVersion pv) + [this, cb=std::move(cb)](bool isOnline, HIDPP::ProtocolVersion pv) mutable { if (!isOnline) { @@ -920,19 +922,19 @@ void SubHidppConnection::checkAndUpdatePresenterState(std::function&& resultMap) { - if (resultMap.size()) { + if (!resultMap.empty()) { for (const auto& res : resultMap) { logDebug(hid) << tr("InitFeature result %1 => %2").arg(toString(res.first)).arg(toString(res.second)); } } setPresenterState(PresenterState::Initialized_Online); - if (cb) cb(m_presenterState); + if (cb) { cb(m_presenterState); } })); } else if (m_presenterState == PresenterState::Initialized_Online) { setPresenterState(PresenterState::Initialized_Online); - if (cb) cb(m_presenterState); + if (cb) { cb(m_presenterState); } } })); }); @@ -974,7 +976,8 @@ void SubHidppConnection::checkAndUpdatePresenterState(std::function(20)); + // size_t{HIDPP::Message } .. to make clang-tidy happy + HIDPP::Message msg(std::vector(size_t{HIDPP::Message::LONG_MSG_SIZE})); const auto res = ::read(fd, msg.data(), msg.dataSize()); if (res < 0) { if (errno != EAGAIN) { diff --git a/src/devicescan.h b/src/devicescan.h index 368aaece..90cb7548 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -19,29 +19,6 @@ struct SupportedDevice QString name = {}; }; -// // ------------------------------------------------------------------------------------------------- -// struct DeviceId -// { -// uint16_t vendorId = 0; -// uint16_t productId = 0; -// BusType busType = BusType::Unknown; -// QString phys; // should be sufficient to differentiate between two devices of the same type -// // - not tested, don't have two devices of any type currently. - -// inline bool operator==(const DeviceId& rhs) const { -// return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); -// } - -// inline bool operator!=(const DeviceId& rhs) const { -// return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); -// } - -// inline bool operator<(const DeviceId& rhs) const { -// return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); -// } -// }; -// Q_DECLARE_METATYPE(DeviceId); - // ------------------------------------------------------------------------------------------------- namespace DeviceScan { diff --git a/src/hidpp.cc b/src/hidpp.cc index 35914c16..2c497f3e 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -2,8 +2,9 @@ // - See LICENSE.md and README.md #include "hidpp.h" -#include "logging.h" + #include "enum-helper.h" +#include "logging.h" #include @@ -16,7 +17,7 @@ namespace { // ----------------------------------------------------------------------------------------------- namespace Defaults { constexpr uint8_t HidppSoftwareId = 7; - } + } // end namespace Defaults // ----------------------------------------------------------------------------------------------- // -- HID++ message offsets @@ -38,13 +39,13 @@ namespace { constexpr uint32_t FwPrefix = FwType + 1; constexpr uint32_t FwVersion = FwPrefix + 3; constexpr uint32_t FwBuild = FwVersion + 2; - } + } // end namespace Offset // ----------------------------------------------------------------------------------------------- namespace Defines { constexpr uint8_t ErrorShort = 0x8f; constexpr uint8_t ErrorLong = 0xff; - } + } // end namespace Defines // ----------------------------------------------------------------------------------------------- uint8_t funcSwIdToByte(uint8_t function, uint8_t swId) { @@ -116,13 +117,13 @@ Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t f uint8_t swId, Data payload) : Message(Data{to_integral(type), deviceIndex, featureIndex, funcSwIdToByte(function, swId)}) { - if (type == Type::Invalid) return; + if (type == Type::Invalid) { return; } m_data.reserve(m_data.size() + payload.size()); std::move(payload.begin(), payload.end(), std::back_inserter(m_data)); - if (type == Type::Long) m_data.resize(20, 0x0); - else if (type == Type::Short) m_data.resize(7, 0x0); + if (type == Type::Long) { m_data.resize(LONG_MSG_SIZE, 0x0); } + else if (type == Type::Short) { m_data.resize(SHORT_MSG_SIZE, 0x0); } } // ------------------------------------------------------------------------------------------------- @@ -144,16 +145,16 @@ Message::Message(Type type, uint8_t deviceIndex, Data payload) // ------------------------------------------------------------------------------------------------- size_t Message::size() const { - if (isLong()) return 20; - if (isShort()) return 7; + if (isLong()) { return LONG_MSG_SIZE; } + if (isShort()) { return SHORT_MSG_SIZE; } return 0; } // ------------------------------------------------------------------------------------------------- Message::Type Message::type() const { - if (isLong()) return Type::Long; - if (isShort()) return Type::Short; + if (isLong()) { return Type::Long; } + if (isShort()) { return Type::Short; } return Type::Invalid; } @@ -162,12 +163,12 @@ bool Message::isValid() const { return isLong() || isShort(); } // ------------------------------------------------------------------------------------------------- bool Message::isShort() const { - return (m_data.size() >= 7 && m_data[Offset::Type] == to_integral(Message::Type::Short)); + return (m_data.size() >= SHORT_MSG_SIZE && m_data[Offset::Type] == to_integral(Message::Type::Short)); } // ------------------------------------------------------------------------------------------------- bool Message::isLong() const { - return (m_data.size() >= 20 && m_data[Offset::Type] == to_integral(Message::Type::Long)); + return (m_data.size() >= LONG_MSG_SIZE && m_data[Offset::Type] == to_integral(Message::Type::Long)); } // ------------------------------------------------------------------------------------------------- @@ -176,9 +177,11 @@ bool Message::isError() const if (isShort() && m_data[Offset::SubId] == Defines::ErrorShort) { return true; } - else if (isLong() && m_data[Offset::SubId] == Defines::ErrorLong) { + + if (isLong() && m_data[Offset::SubId] == Defines::ErrorLong) { return true; } + return false; } @@ -270,7 +273,7 @@ void Message::setSoftwareId(uint8_t softwareId) { // ------------------------------------------------------------------------------------------------- bool Message::isResponseTo(const Message& other) const { - if (!isValid() || !other.isValid()) return false; + if (!isValid() || !other.isValid()) { return false; } return deviceIndex() == other.deviceIndex() && subId() == other.subId() @@ -280,7 +283,7 @@ bool Message::isResponseTo(const Message& other) const // ------------------------------------------------------------------------------------------------- bool Message::isErrorResponseTo(const Message& other) const { - if (!isValid() || !other.isValid()) return false; + if (!isValid() || !other.isValid()) { return false; } return deviceIndex() == other.deviceIndex() && errorSubId() == other.subId() @@ -290,10 +293,10 @@ bool Message::isErrorResponseTo(const Message& other) const // ------------------------------------------------------------------------------------------------- Message& Message::convertToLong() { - if (!isShort()) return *this; + if (!isShort()) { return *this; } // Resize data vector, pad with zeroes. - m_data.resize(20, 0); + m_data.resize(LONG_MSG_SIZE, 0); m_data[Offset::Type] = to_integral(Type::Long); return *this; } @@ -325,7 +328,7 @@ FeatureSet::State FeatureSet::state() const { // ------------------------------------------------------------------------------------------------- void FeatureSet::setState(State s) { - if (s == m_state) return; + if (s == m_state) { return; } m_state = s; emit stateChanged(m_state); @@ -334,26 +337,26 @@ void FeatureSet::setState(State s) // ------------------------------------------------------------------------------------------------- void FeatureSet::getFeatureIndex(FeatureCode fc, std::function cb) { - postSelf([this, fc, cb=std::move(cb)]() + postSelf([this, fc, cb=std::move(cb)]() mutable { if (m_connection == nullptr) { - if (cb) cb(MsgResult::WriteError, 0); + if (cb) { cb(MsgResult::WriteError, 0); } return; } - const uint8_t fcLSB = static_cast(to_integral(fc) >> 8); - const uint8_t fcMSB = static_cast(to_integral(fc) & 0x00ff); + const auto fcLSB = static_cast(to_integral(fc) >> 8); + const auto fcMSB = static_cast(to_integral(fc) & 0x00ff); Message featureIndexReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, Message::Data{fcLSB, fcMSB}); m_connection->sendRequest(std::move(featureIndexReqMsg), - [cb=std::move(cb), fc, fcLSB, fcMSB](MsgResult result, Message&& msg) + [cb=std::move(cb), fc](MsgResult result, Message&& msg) { logDebug(hid) << tr("getFeatureIndex(%1) => %2, %3") .arg(to_integral(fc)).arg(toString(result)).arg(msg[4]); - if (cb) cb(result, (result != MsgResult::Ok) ? 0 : msg[4]); + if (cb) { cb(result, (result != MsgResult::Ok) ? 0 : msg[4]); } }); }); } @@ -362,11 +365,11 @@ void FeatureSet::getFeatureIndex(FeatureCode fc, std::function cb) { getFeatureIndex(FeatureCode::FeatureSet, makeSafeCallback( - [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) mutable { if (res != MsgResult::Ok) { - if (cb) cb(res, 0, 0); + if (cb) { cb(res, 0, 0); } return; } @@ -374,7 +377,7 @@ void FeatureSet::getFeatureCount(std::functionsendRequest(std::move(featureCountReqMsg), [featureIndex, cb=std::move(cb)](MsgResult result, Message&& msg) { - if (cb) cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); + if (cb) { cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); } }); })); } @@ -383,11 +386,11 @@ void FeatureSet::getFeatureCount(std::function cb) { getFeatureIndex(FeatureCode::FirmwareVersion, makeSafeCallback( - [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) mutable { if (res != MsgResult::Ok) { - if (cb) cb(res, 0, 0); + if (cb) { cb(res, 0, 0); } return; } @@ -398,7 +401,7 @@ void FeatureSet::getFirmwareCount(std::function %1, featureIndex = %2, count = %3") .arg(toString(result)).arg(featureIndex).arg(msg[4]); - if (cb) cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); + if (cb) { cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); } }); })); } @@ -409,7 +412,7 @@ void FeatureSet::getFirmwareInfo(uint8_t fwIndex, uint8_t entity, { if (m_connection == nullptr) { - if (cb) cb(MsgResult::WriteError, FirmwareInfo()); + if (cb) { cb(MsgResult::WriteError, FirmwareInfo()); } return; } @@ -418,7 +421,7 @@ void FeatureSet::getFirmwareInfo(uint8_t fwIndex, uint8_t entity, m_connection->sendRequest(std::move(fwVerReqMessage), [cb=std::move(cb)](MsgResult res, Message&& msg) { - if (cb) cb(res, FirmwareInfo(std::move(msg))); + if (cb) { cb(res, FirmwareInfo(std::move(msg))); } }); } @@ -426,11 +429,11 @@ void FeatureSet::getFirmwareInfo(uint8_t fwIndex, uint8_t entity, void FeatureSet::getMainFirmwareInfo(std::function cb) { getFirmwareCount(makeSafeCallback( - [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) mutable { if (res != MsgResult::Ok) { - if (cb) cb(res, FirmwareInfo()); + if (cb) { cb(res, FirmwareInfo()); } return; } getMainFirmwareInfo(featureIndex, count, 0, std::move(cb)); @@ -442,7 +445,7 @@ void FeatureSet::getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t curre std::function cb) { getFirmwareInfo(fwIndex, current, makeSafeCallback( - [this, current, max, fwIndex, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) + [this, current, max, fwIndex, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) mutable { logDebug(hid) << tr("getFirmwareInfo(%1, %2, %3) => %4, fi.type = %5, fi.ver = %6, fi.pref = %7") .arg(fwIndex).arg(max).arg(current).arg(toString(res)) @@ -450,12 +453,12 @@ void FeatureSet::getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t curre if (res == MsgResult::Ok && fi.firmwareType() == FirmwareInfo::FirmwareType::MainApp) { - if (cb) cb(res, std::move(fi)); + if (cb) { cb(res, std::move(fi)); } return; } if (max == current + 1) { - if (cb) cb(res, FirmwareInfo()); + if (cb) { cb(res, FirmwareInfo()); } return; } @@ -466,17 +469,17 @@ void FeatureSet::getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t curre // ------------------------------------------------------------------------------------------------- void FeatureSet::initFromDevice(std::function cb) { - postSelf([this, cb=std::move(cb)]() + postSelf([this, cb=std::move(cb)]() mutable { if (m_connection == nullptr || m_state == State::Initialized || m_state == State::Initializing) { - if (cb) cb(m_state); + if (cb) { cb(m_state); } return; } setState(State::Initializing); - getMainFirmwareInfo(makeSafeCallback([this, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) + getMainFirmwareInfo(makeSafeCallback([this, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) mutable { logDebug(hid) << tr("getMainFirmwareInfo() => %1, fi.type = %2").arg(toString(res)) .arg(to_integral(fi.firmwareType())); @@ -489,7 +492,7 @@ void FeatureSet::initFromDevice(std::function cb) Q_UNUSED(res); getFeatureCount(makeSafeCallback( - [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) mutable { logDebug(hid) << tr("getFeatureCount() => %1, featureIndex = %2, count = %3") .arg(toString(res)).arg(featureIndex).arg(count); @@ -497,7 +500,7 @@ void FeatureSet::initFromDevice(std::function cb) if (res != MsgResult::Ok) { setState(State::Error); - if (cb) cb(m_state); + if (cb) { cb(m_state); } return; } @@ -513,7 +516,7 @@ void FeatureSet::initFromDevice(std::function cb) setState(State::Initialized); } - if (cb) cb(m_state); + if (cb) { cb(m_state); } })); // getFeatureIds (table) })); // getFeatureCount })); // getMainFwInfo @@ -526,13 +529,13 @@ void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, { if (m_connection == nullptr) { - if (cb) cb(MsgResult::WriteError, FeatureTable{}); // empty featuretable + if (cb) { cb(MsgResult::WriteError, FeatureTable{}); } // empty featuretable return; } if (count == 0) { - if (cb) cb(MsgResult::Ok, FeatureTable{}); // no count, empty featuretable + if (cb) { cb(MsgResult::Ok, FeatureTable{}); }// no count, empty featuretable return; } @@ -541,20 +544,18 @@ void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, HidppConnectionInterface::RequestBatch batch; for (uint8_t featureIndex = 1; featureIndex <= count; ++featureIndex) { - // featureIdReqMsg[Offset::Payload] = featureIndex; batch.emplace(HidppConnectionInterface::RequestBatchItem { Message(Message::Type::Long, DeviceIndex::WirelessDevice1, featureSetIndex, 1, Message::Data{featureIndex}), [featureTable, featureIndex](MsgResult res, Message&& msg) { - if (res != MsgResult::Ok) return; + if (res != MsgResult::Ok) { return; } const uint16_t featureCode = (static_cast(msg[4]) << 8) | static_cast(msg[5]); const uint8_t featureType = msg[6]; const bool softwareHidden = (featureType & (1<<6)); const bool obsoleteFeature = (featureType & (1<<7)); if (!softwareHidden && !obsoleteFeature) { - // logDebug(hid) << tr("featureCode %1 -> index %2").arg(featureCode).arg(featureIndex); featureTable->emplace(featureCode, featureIndex); } } @@ -563,7 +564,7 @@ void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, m_connection->sendRequestBatch(std::move(batch), [featureTable, cb=std::move(cb)](std::vector&& results) { - if (cb) cb(results.back(), std::move(*featureTable)); + if (cb) { cb(results.back(), std::move(*featureTable)); } }); } @@ -592,7 +593,7 @@ FirmwareInfo::FirmwareInfo(Message&& msg) // ------------------------------------------------------------------------------------------------- FirmwareInfo::FirmwareType FirmwareInfo::firmwareType() const { - if (!m_rawMsg.isLong()) return FirmwareType::Invalid; + if (!m_rawMsg.isLong()) { return FirmwareType::Invalid; } switch(m_rawMsg[Offset::Payload] & 0xf) { diff --git a/src/hidpp.h b/src/hidpp.h index 0dfc4ce6..51d0ac62 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -131,6 +131,9 @@ namespace HIDPP { class Message final { public: + static constexpr int SHORT_MSG_SIZE = 7; + static constexpr int LONG_MSG_SIZE = 20; + using Data = std::vector; /// HID++ message type. @@ -212,7 +215,6 @@ namespace HIDPP { bool isErrorResponseTo(const Message& other) const; auto data() { return m_data.data(); } - const auto data() const { return m_data.data(); } auto dataSize() { return m_data.size(); } auto& operator[](size_t i) { return m_data.operator[](i); } const auto& operator[](size_t i) const { return m_data.operator[](i); } From f408b82bdbbb494adf829f72280ba4779f3d012e Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sat, 18 Sep 2021 10:54:52 +0900 Subject: [PATCH 073/110] Minor code fixes --- src/deviceswidget.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index ef885da2..9e924323 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -129,7 +129,7 @@ QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) const auto diWidget = new DeviceInfoWidget(this); connect(this, &DevicesWidget::currentDeviceChanged, this, - [this, diWidget, spotlight](const DeviceId& dId) { + [diWidget, spotlight](const DeviceId& dId) { diWidget->setDeviceConnection(spotlight->deviceConnection(dId).get()); }); @@ -499,7 +499,7 @@ void DeviceInfoWidget::connectToBatteryUpdates(SubHidppConnection* hdc) delayedTextEditUpdate(); }); - connect(m_batteryInfoTimer, &QTimer::timeout, m_connectionContext, [this, hdc]() { + connect(m_batteryInfoTimer, &QTimer::timeout, m_connectionContext, [hdc]() { hdc->triggerBattyerInfoUpdate(); }); } From cba7a2cef4fd3547b99fe32cb6c6078f49706bf5 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 21 Sep 2021 21:08:21 +0200 Subject: [PATCH 074/110] Fix clang-tidy warnings where feasible (pt2) --- .clang-tidy | 8 +- src/actiondelegate.cc | 83 ++++--- src/device-hidpp.cc | 2 +- src/device-hidpp.h | 4 +- src/device-vibration.cc | 87 +++++--- src/device-vibration.h | 22 +- src/device.cc | 24 ++- src/deviceinput.cc | 102 +++++---- src/devicescan.cc | 29 +-- src/deviceswidget.cc | 32 +-- src/deviceswidget.h | 2 +- src/extra-devices.cc.in | 2 +- src/hidpp.cc | 6 +- src/iconwidgets.cc | 2 +- src/inputmapconfig.cc | 46 ++-- src/inputseqedit.cc | 75 +++---- src/linuxdesktop.cc | 9 +- src/logging.cc | 40 ++-- src/logging.h | 2 +- src/main.cc | 466 +++++++++++++++++++++++----------------- src/nativekeyseqedit.cc | 36 ++-- src/preferencesdlg.cc | 27 +-- src/projecteurapp.cc | 284 ++++++++++++------------ src/projecteurapp.h | 3 + src/settings.cc | 87 ++++---- src/spotlight.cc | 56 ++--- src/spotshapes.cc | 18 +- src/virtualdevice.cc | 6 +- 28 files changed, 844 insertions(+), 716 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 366832e2..6f069ef0 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -12,10 +12,12 @@ Checks: > -cppcoreguidelines-pro-type-vararg,-hicpp-vararg, -cppcoreguidelines-pro-type-static-cast-downcast, -cppcoreguidelines-pro-bounds-pointer-arithmetic, - -readability-implicit-bool-conversion, - -hicpp-signed-bitwise, + -readability-implicit-bool-conversion, - readability-container-size-empty, + -hicpp-signed-bitwise, -cppcoreguidelines-macro-usage, -cppcoreguidelines-avoid-c-arrays,-hicpp-avoid-c-arrays,-modernize-avoid-c-arrays, - -google-default-arguments + -google-default-arguments,-google-readability-todo, + -hicpp-uppercase-literal-suffix,-readability-uppercase-literal-suffix, + -clang-analyzer-core.CallAndMessage, -readability-static-accessed-through-instance WarningsAsErrors: false CheckOptions: diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index ad0737fc..88ff1eb7 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -35,9 +35,9 @@ namespace { const int w = std::max(opt.fontMetrics.width(ActionDelegate::tr("None")) + 2 * horizontalMargin, opt.fontMetrics.width(action->keySequence.toString())); #endif - return QSize(w, h); + return { w, h }; } - } + } // end namespace keysequence namespace cyclepresets { // --------------------------------------------------------------------------------------------- @@ -49,11 +49,10 @@ namespace { } // --------------------------------------------------------------------------------------------- - QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const CyclePresetsAction* /*action*/) - { - return QSize(100,16); + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const CyclePresetsAction* /*action*/) { + return { 100, 16 }; } - } + } // end namespace cyclepresets namespace togglespotlight { // --------------------------------------------------------------------------------------------- @@ -65,11 +64,10 @@ namespace { } // --------------------------------------------------------------------------------------------- - QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ToggleSpotlightAction* /*action*/) - { - return QSize(100,16); + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ToggleSpotlightAction* /*action*/) { + return { 100, 16 }; } - } + } // end namespace togglespotlight namespace scrollhorizontal { // --------------------------------------------------------------------------------------------- @@ -81,11 +79,10 @@ namespace { } // --------------------------------------------------------------------------------------------- - QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollHorizontalAction* /*action*/) - { - return QSize(100,16); + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollHorizontalAction* /*action*/) { + return { 100, 16 }; } - } + } // end namespace scrollhorizontal namespace scrollvertical { // --------------------------------------------------------------------------------------------- @@ -97,11 +94,10 @@ namespace { } // --------------------------------------------------------------------------------------------- - QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollVerticalAction* /*action*/) - { - return QSize(100,16); + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollVerticalAction* /*action*/) { + return { 100, 16 }; } - } + } // end namespace scrollvertical namespace volumecontrol { // --------------------------------------------------------------------------------------------- @@ -113,11 +109,10 @@ namespace { } // --------------------------------------------------------------------------------------------- - QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const VolumeControlAction* /*action*/) - { - return QSize(100,16); + QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const VolumeControlAction* /*action*/) { + return { 100, 16 }; } - } + } // end namespace volumecontrol } // end anonymous namespace // ------------------------------------------------------------------------------------------------- @@ -164,7 +159,8 @@ void ActionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option QSize ActionDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelIndex& index) const { const auto imModel = qobject_cast(index.model()); - if (!imModel) return QStyledItemDelegate::sizeHint(opt, index); + if (!imModel) { return QStyledItemDelegate::sizeHint(opt, index); } + const auto& item = imModel->configData(index); if (!item.action) { return QStyledItemDelegate::sizeHint(opt, index); } @@ -198,16 +194,12 @@ QWidget* ActionDelegate::createEditor(QWidget* parent, const Action* action) con connect(editor, &NativeKeySeqEdit::editingFinished, this, &ActionDelegate::commitAndCloseEditor); return editor; } - case Action::Type::CyclePresets: // None for now... - break; - case Action::Type::ToggleSpotlight: // None for now... - break; - case Action::Type::ScrollHorizontal: // None for now... - break; - case Action::Type::ScrollVertical: // None for now... - break; - case Action::Type::VolumeControl: // None for now... - break; + case Action::Type::CyclePresets: // [[fallthrough]]; + case Action::Type::ToggleSpotlight: // [[fallthrough]]; + case Action::Type::ScrollHorizontal: // [[fallthrough]]; + case Action::Type::ScrollVertical: // [[fallthrough]]; + case Action::Type::VolumeControl: + break; // No editor } return nullptr; } @@ -218,7 +210,7 @@ QWidget* ActionDelegate::createEditor(QWidget* parent, const QStyleOptionViewIte { const auto imModel = qobject_cast(index.model()); - if (!imModel) return nullptr; + if (!imModel) { return nullptr; } const auto& item = imModel->configData(index); if (!item.action) { return nullptr; } @@ -264,7 +256,7 @@ bool ActionDelegate::eventFilter(QObject* obj, QEvent* ev) { // Let all key press events pass through to the editor, // otherwise some keys cannot be recorded as a key sequence (e.g. [Tab] and [Esc]) - if (qobject_cast(obj)) return false; + if (qobject_cast(obj)) { return false; } } return QStyledItemDelegate::eventFilter(obj,ev); } @@ -286,11 +278,11 @@ void ActionDelegate::commitAndCloseEditor_() void ActionDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos) { - if (!index.isValid() || !model) return; + if (!index.isValid() || !model) { return; } const auto& item = model->configData(index); - if (!item.action || item.action->type() != Action::Type::KeySequence) return; + if (!item.action || item.action->type() != Action::Type::KeySequence) { return; } - QMenu* menu = new QMenu(parent); + auto* const menu = new QMenu(parent); const std::vector predefinedKeys = { &NativeKeySequence::predefined::altTab(), &NativeKeySequence::predefined::altF4(), @@ -333,8 +325,9 @@ void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op return 0; }(); - if (symbol != 0) + if (symbol != 0) { drawActionTypeSymbol(0, *painter, option, symbol); + } if (option.state & QStyle::State_HasFocus) { InputSeqDelegate::drawCurrentIndicator(*painter, option); @@ -345,9 +338,10 @@ void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos) { - if (!index.isValid() || !model) return; + if (!index.isValid() || !model) { return; } + const auto& item = model->configData(index); - if (!item.action) return; + if (!item.action) { return; } const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); const bool isSpecialMoveInput = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), @@ -393,7 +387,7 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* return true; }(); - QMenu* menu = new QMenu(parent); + auto* const menu = new QMenu(parent); for (const auto& entry : items) { if ((isSpecialMoveInput && entry.isMoveAction) @@ -423,10 +417,11 @@ int ActionTypeDelegate::drawActionTypeSymbol(int startX, QPainter& p, p.setFont(iconFont); p.setRenderHint(QPainter::Antialiasing, true); - if (option.state & QStyle::State_Selected) + if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::HighlightedText)); - else + } else { p.setPen(option.palette.color(QPalette::Text)); + } QRect br; p.drawText(r, Qt::AlignHCenter | Qt::AlignVCenter, QString(symbol), &br); diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index 472bf860..55e85574 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -293,7 +293,7 @@ void SubHidppConnection::registerNotificationCallback(QObject* obj, uint8_t feat { if (obj == nullptr || !cb) { return; } - postSelf([this, obj, featureIndex, function, cb=std::move(cb)]() + postSelf([this, obj, featureIndex, function, cb=std::move(cb)]() mutable { auto& callbackList = m_notificationSubscribers[featureIndex]; callbackList.emplace_back(Subscriber{obj, function, std::move(cb)}); diff --git a/src/device-hidpp.h b/src/device-hidpp.h index fbebb2e6..bf9791c8 100644 --- a/src/device-hidpp.h +++ b/src/device-hidpp.h @@ -47,7 +47,7 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void sendData(HIDPP::Message msg, SendResultCallback resultCb) override; void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, bool continueOnError = false) override; - void sendRequest(std::vector msg, RequestResultCallback responseCb) override; + void sendRequest(std::vector data, RequestResultCallback responseCb) override; void sendRequest(HIDPP::Message msg, RequestResultCallback responseCb) override; void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, bool continueOnError = false) override; @@ -107,7 +107,7 @@ class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInt void clearTimedOutRequests(); - void sendDataBatch(DataBatch requestBatch, DataBatchResultCallback cb, bool continueOnError, + void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, bool continueOnError, std::vector results); void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, bool continueOnError, std::vector results); diff --git a/src/device-vibration.cc b/src/device-vibration.cc index ea75da48..a315b025 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -26,14 +26,14 @@ DECLARE_LOGGING_CATEGORY(hid) // ------------------------------------------------------------------------------------------------- namespace { - constexpr int numTimers = 3; -} + constexpr uint32_t numTimers = 3; +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- struct TimerWidget::Impl { // ----------------------------------------------------------------------------------------------- - Impl(TimerWidget* parent) + explicit Impl(TimerWidget* parent) : stack(new QStackedWidget(parent)) , editor(new QWidget(parent)) , overlay(new QWidget(parent)) @@ -64,9 +64,14 @@ struct TimerWidget::Impl editLayout->addWidget(new QLabel(TimerWidget::tr("s"), editor)); editLayout->addStretch(1); - sbHours->setRange(0, 24); - sbMinutes->setRange(0, 59); - sbSeconds->setRange(0, 59); + constexpr auto day = std::chrono::hours(24); + constexpr auto hoursMax = (day - std::chrono::hours(1)).count(); + constexpr auto minutesMax = std::chrono::minutes(60).count() - 1; + constexpr auto secondsMax = std::chrono::seconds(60).count() - 1; + + sbHours->setRange(0, hoursMax); + sbMinutes->setRange(0, minutesMax); + sbSeconds->setRange(0, secondsMax); layout->addWidget(btnStartStop); btnStartStop->setCheckable(true); @@ -94,7 +99,7 @@ struct TimerWidget::Impl btnStartStop->setEnabled(checkbox->isChecked()); QObject::connect(checkbox, &QCheckBox::toggled, parent, [this, parent](bool checked) { editor->setEnabled(checked); - if (!checked) btnStartStop->setChecked(false); + if (!checked) { btnStartStop->setChecked(false); } btnStartStop->setEnabled(checked); emit parent->enabledChanged(checked); }); @@ -190,8 +195,9 @@ bool TimerWidget::timerRunning() const { // ------------------------------------------------------------------------------------------------- void TimerWidget::start() { - if (timerEnabled()) + if (timerEnabled()) { m_impl->btnStartStop->setChecked(true); + } } // ------------------------------------------------------------------------------------------------- @@ -206,9 +212,9 @@ void TimerWidget::setValueSeconds(int seconds) const auto hours = std::chrono::duration_cast(totalSecs); const auto mins = std::chrono::duration_cast(totalSecs-hours); const auto secs = std::chrono::duration_cast(totalSecs-hours-mins); - m_impl->sbHours->setValue(hours.count()); - m_impl->sbMinutes->setValue(mins.count()); - m_impl->sbSeconds->setValue(secs.count()); + m_impl->sbHours->setValue( static_cast(hours.count()) ); + m_impl->sbMinutes->setValue( static_cast(mins.count()) ); + m_impl->sbSeconds->setValue( static_cast(secs.count()) ); } // ------------------------------------------------------------------------------------------------- @@ -224,7 +230,7 @@ int TimerWidget::valueSeconds() const { // ------------------------------------------------------------------------------------------------- struct MultiTimerWidget::Impl { - Impl(QWidget* parent) + explicit Impl(QWidget* parent) { for (size_t i = 0; i < numTimers; ++i) { timers.at(i) = new TimerWidget(parent); @@ -239,6 +245,8 @@ MultiTimerWidget::MultiTimerWidget(QWidget* parent) : QWidget(parent) , m_impl(new Impl(this)) { + constexpr int defaultTimeoutIncrMin = 15; + const auto layout = new QHBoxLayout(this); const auto iconLabel = new IconLabel(Font::time_19, this); layout->addWidget(iconLabel); @@ -251,15 +259,21 @@ MultiTimerWidget::MultiTimerWidget(QWidget* parent) layout->setAlignment(groupBox, Qt::AlignTop); const auto timerLayout = new QVBoxLayout(groupBox); - for (size_t i = 0; i < numTimers; ++i) { + for (uint32_t i = 0; i < numTimers; ++i) + { timerLayout->addWidget(m_impl->timers.at(i)); - m_impl->timers.at(i)->setValueMinutes(15 + i * 15); + const auto timerDefaultValueMinutes = defaultTimeoutIncrMin + i * defaultTimeoutIncrMin; + + m_impl->timers.at(i)->setValueMinutes(static_cast(timerDefaultValueMinutes)); + connect(m_impl->timers.at(i), &TimerWidget::valueSecondsChanged, this, [this, i](int secs) { emit timerValueChanged(i, secs); }); + connect(m_impl->timers.at(i), &TimerWidget::enabledChanged, this, [this, i](bool enabled) { emit timerEnabledChanged(i, enabled); }); + connect(m_impl->timers.at(i), &TimerWidget::timeout, this, [this, i](){ emit timeout(i); }); @@ -272,35 +286,35 @@ MultiTimerWidget::MultiTimerWidget(QWidget* parent) MultiTimerWidget::~MultiTimerWidget() = default; // ------------------------------------------------------------------------------------------------- -int MultiTimerWidget::timerCount() const { +int MultiTimerWidget::timerCount() { return numTimers; } // ------------------------------------------------------------------------------------------------- -void MultiTimerWidget::setTimerEnabled(int timerId, bool enabled) +void MultiTimerWidget::setTimerEnabled(uint32_t timerId, bool enabled) { - if (timerId < 0 || timerId >= numTimers) return; + if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->setTimerEnabled(enabled); } // ------------------------------------------------------------------------------------------------- -bool MultiTimerWidget::timerEnabled(int timerId) const +bool MultiTimerWidget::timerEnabled(uint32_t timerId) const { - if (timerId < 0 || timerId >= numTimers) return false; + if (timerId >= numTimers) { return false; } return m_impl->timers.at(timerId)->timerEnabled(); } // ------------------------------------------------------------------------------------------------- -void MultiTimerWidget::startTimer(int timerId) +void MultiTimerWidget::startTimer(uint32_t timerId) { - if (timerId < 0 || timerId >= numTimers) return; + if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->start(); } // ------------------------------------------------------------------------------------------------- -void MultiTimerWidget::stopTimer(int timerId) +void MultiTimerWidget::stopTimer(uint32_t timerId) { - if (timerId < 0 || timerId >= numTimers) return; + if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->stop(); } @@ -313,23 +327,23 @@ void MultiTimerWidget::stopAllTimers() } // ------------------------------------------------------------------------------------------------- -bool MultiTimerWidget::timerRunning(int timerId) const +bool MultiTimerWidget::timerRunning(uint32_t timerId) const { - if (timerId < 0 || timerId >= numTimers) return false; + if (timerId >= numTimers) { return false; } return m_impl->timers.at(timerId)->timerRunning(); } // ------------------------------------------------------------------------------------------------- -void MultiTimerWidget::setTimerValue(int timerId, int seconds) +void MultiTimerWidget::setTimerValue(uint32_t timerId, int seconds) { - if (timerId < 0 || timerId >= numTimers) return; + if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->setValueSeconds(seconds); } // ------------------------------------------------------------------------------------------------- -int MultiTimerWidget::timerValue(int timerId) const +int MultiTimerWidget::timerValue(uint32_t timerId) const { - if (timerId < 0 || timerId >= numTimers) return -1; + if (timerId >= numTimers) { return -1; } return m_impl->timers.at(timerId)->valueSeconds(); } @@ -339,8 +353,11 @@ VibrationSettingsWidget::VibrationSettingsWidget(QWidget* parent) , m_sbLength(new QSpinBox(this)) , m_sbIntensity(new QSpinBox(this)) { + constexpr int vibrationIntensityMin = 25; + constexpr int vibrationIntensityMax = 255; + m_sbLength->setRange(0, 10); - m_sbIntensity->setRange(25, 255); + m_sbIntensity->setRange(vibrationIntensityMin, vibrationIntensityMax); const auto layout = new QHBoxLayout(this); const auto iconLabel = new IconLabel(Font::control_panel_9, this); @@ -395,14 +412,14 @@ uint8_t VibrationSettingsWidget::intensity() const { // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setLength(uint8_t len) { - if (m_sbLength->value() == len) return; + if (m_sbLength->value() == len) { return; } m_sbLength->setValue(len); } // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setIntensity(uint8_t intensity) { - if (m_sbIntensity->value() == intensity) return; + if (m_sbIntensity->value() == intensity) { return; } m_sbIntensity->setValue(intensity); } @@ -415,9 +432,9 @@ void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc) // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::sendVibrateCommand() { - if (!m_subDeviceConnection) return; - if (!m_subDeviceConnection->isConnected()) return; - if (!m_subDeviceConnection->hasFlags(DeviceFlag::Vibrate)) return; + if (!m_subDeviceConnection) { return; } + if (!m_subDeviceConnection->isConnected()) { return; } + if (!m_subDeviceConnection->hasFlags(DeviceFlag::Vibrate)) { return; } const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); diff --git a/src/device-vibration.h b/src/device-vibration.h index bd753491..097a5c20 100644 --- a/src/device-vibration.h +++ b/src/device-vibration.h @@ -49,24 +49,24 @@ class MultiTimerWidget : public QWidget virtual ~MultiTimerWidget() override; /// Returns the number of timers - int timerCount() const; + static int timerCount(); - void setTimerEnabled(int timerId, bool enabled); - bool timerEnabled(int timerId) const; + void setTimerEnabled(uint32_t timerId, bool enabled); + bool timerEnabled(uint32_t timerId) const; - void startTimer(int timerId); - void stopTimer(int timerId); + void startTimer(uint32_t timerId); + void stopTimer(uint32_t timerId); void stopAllTimers(); - bool timerRunning(int timerId) const; + bool timerRunning(uint32_t timerId) const; - void setTimerValue(int timerId, int seconds); - int timerValue(int timerId) const; + void setTimerValue(uint32_t timerId, int seconds); + int timerValue(uint32_t timerId) const; signals: /// Emitted when a timer times out. - void timeout(int timerId); - void timerEnabledChanged(int timerId, bool enabled); - void timerValueChanged(int timerId, int seconds); + void timeout(uint32_t timerId); + void timerEnabledChanged(uint32_t timerId, bool enabled); + void timerValueChanged(uint32_t timerId, int seconds); private: struct Impl; diff --git a/src/device.cc b/src/device.cc index 56e39d47..74c14229 100644 --- a/src/device.cc +++ b/src/device.cc @@ -3,9 +3,9 @@ #include "device.h" -#include "enum-helper.h" #include "deviceinput.h" #include "devicescan.h" +#include "enum-helper.h" #include "hidpp.h" #include "logging.h" @@ -25,7 +25,7 @@ namespace { const auto hexId = logging::hexId; // class i18n : public QObject {}; // for i18n and logging -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- const char* toString(BusType bt, bool withClass) @@ -81,7 +81,7 @@ bool DeviceConnection::hasSubDevice(const QString& path) const // ------------------------------------------------------------------------------------------------- void DeviceConnection::addSubDevice(std::shared_ptr sdc) { - if (!sdc) return; + if (!sdc) { return; } const auto path = sdc->path(); connect(&*sdc, &SubDeviceConnection::flagsChanged, this, [this, path](){ @@ -180,7 +180,8 @@ QSocketNotifier* SubDeviceConnection::socketReadNotifier() { } // ------------------------------------------------------------------------------------------------- -SubEventConnection::SubEventConnection(Token, const DeviceId& dId, const DeviceScan::SubDevice& sd) +SubEventConnection::SubEventConnection(Token /* token */, + const DeviceId& dId, const DeviceScan::SubDevice& sd) : SubDeviceConnection(dId, sd, ConnectionType::Event, ConnectionMode::ReadOnly) {} // ------------------------------------------------------------------------------------------------- @@ -225,9 +226,9 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: auto connection = std::make_shared(Token{}, dc.deviceId(), sd); - if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents; - if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents; - if (!!(bitmask & (1 << EV_KEY))) connection->m_details.deviceFlags |= DeviceFlag::KeyEvents; + if (!!(bitmask & (1 << EV_SYN))) { connection->m_details.deviceFlags |= DeviceFlag::SynEvents; } + if (!!(bitmask & (1 << EV_REP))) { connection->m_details.deviceFlags |= DeviceFlag::RepEvents; } + if (!!(bitmask & (1 << EV_KEY))) { connection->m_details.deviceFlags |= DeviceFlag::KeyEvents; } if (!!(bitmask & (1 << EV_REL))) { unsigned long relEvents = 0; @@ -277,7 +278,8 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: } // ------------------------------------------------------------------------------------------------- -SubHidrawConnection::SubHidrawConnection(Token, const DeviceId& dId, const DeviceScan::SubDevice& sd) +SubHidrawConnection::SubHidrawConnection(Token /* token */, + const DeviceId& dId, const DeviceScan::SubDevice& sd) : SubDeviceConnection(dId, sd, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} // ------------------------------------------------------------------------------------------------- @@ -302,7 +304,7 @@ std::shared_ptr SubHidrawConnection::create(const DeviceSca const DeviceConnection& dc) { const int devfd = openHidrawSubDevice(sd, dc.deviceId()); - if (devfd == -1) return std::shared_ptr(); + if (devfd == -1) { return std::shared_ptr(); } auto connection = std::make_shared(Token{}, dc.deviceId(), sd); connection->createSocketNotifiers(devfd, sd.deviceFile); @@ -354,8 +356,8 @@ int SubHidrawConnection::openHidrawSubDevice(const DeviceScan::SubDevice& sd, co }; // Check against given device id - if (static_cast(devinfo.vendor) != devId.vendorId - || static_cast(devinfo.product) != devId.productId) + if (static_cast(devinfo.vendor) != devId.vendorId + || static_cast(devinfo.product) != devId.productId) { logDebug(device) << tr("Device id mismatch: %1 (%2:%3)") .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product)); diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 15138d82..b9c09c01 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -20,8 +20,8 @@ LOGGING_CATEGORY(input, "input") namespace { // ----------------------------------------------------------------------------------------------- - static auto registered_ = qRegisterMetaTypeStreamOperators() - && qRegisterMetaTypeStreamOperators(); + const auto registered_ = qRegisterMetaTypeStreamOperators() + && qRegisterMetaTypeStreamOperators(); // ----------------------------------------------------------------------------------------------- void addKeyToString(QString& str, const QString& key) @@ -54,7 +54,7 @@ namespace { return KeyEventSequence{std::move(pressed)}; }; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- DeviceInputEvent::DeviceInputEvent(const struct input_event& ie) @@ -90,10 +90,10 @@ QDataStream& operator>>(QDataStream& s, DeviceInputEvent& die) { } // ------------------------------------------------------------------------------------------------- -QDebug operator<<(QDebug debug, const DeviceInputEvent &d) +QDebug operator<<(QDebug debug, const DeviceInputEvent &ie) { QDebugStateSaver saver(debug); - debug.nospace() << '{' << d.type << ", " << d.code << ", " << d.value << '}'; + debug.nospace() << '{' << ie.type << ", " << ie.code << ", " << ie.value << '}'; return debug; } @@ -102,8 +102,9 @@ QDebug operator<<(QDebug debug, const KeyEvent &ke) { QDebugStateSaver saver(debug); debug.nospace() << "["; - for (const auto& e : ke) + for (const auto& e : ke) { debug.nospace() << e << ','; + } debug.nospace() << "]"; return debug; } @@ -130,10 +131,15 @@ std::shared_ptr GlobalActions::volumeControl() } // ------------------------------------------------------------------------------------------------- -QDataStream& operator>>(QDataStream& s, MappedAction& mia) { - std::underlying_type_t type; - s >> type; - switch (static_cast(type)) +QDataStream& operator>>(QDataStream& s, MappedAction& mia) +{ + const auto type = [&s](){ + auto type = to_integral(Action::Type::KeySequence); + s >> type; + return type; + }(); + + switch (to_enum(type)) { case Action::Type::KeySequence: mia.action = std::make_shared(); @@ -160,9 +166,9 @@ QDataStream& operator>>(QDataStream& s, MappedAction& mia) { // ------------------------------------------------------------------------------------------------- bool MappedAction::operator==(const MappedAction& o) const { - if (!action && !o.action) return true; - if (!action || !o.action) return false; - if (action->type() != o.action->type()) return false; + if (!action && !o.action) { return true; } + if (!action || !o.action) { return false; } + if (action->type() != o.action->type()) { return false; } switch(action->type()) { case Action::Type::KeySequence: @@ -197,7 +203,7 @@ QDataStream& operator<<(QDataStream& s, const MappedAction& mia) { // ------------------------------------------------------------------------------------------------- namespace { struct KeyEventItem { - KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} + explicit KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} const KeyEvent keyEvent; std::shared_ptr action; std::vector nextMap; @@ -205,7 +211,7 @@ namespace { struct DeviceKeyMap { - DeviceKeyMap(const InputMapConfig& config = {}) { reconfigure(config); } + explicit DeviceKeyMap(const InputMapConfig& config = {}) { reconfigure(config); } enum Result : uint8_t { Miss, Valid, Hit, PartialHit @@ -216,20 +222,20 @@ namespace { auto state() const { return m_pos; } void resetState(); void reconfigure(const InputMapConfig& config = {}); - bool hasConfig() const { return m_rootItem.nextMap.size(); } + bool hasConfig() const { return !m_rootItem.nextMap.empty(); } private: std::list m_items; KeyEventItem m_rootItem; const KeyEventItem* m_pos = &m_rootItem; }; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], size_t num) { - if (!hasConfig()) return Result::Miss; - if (!m_pos) return Result::Miss; + if (!hasConfig()) { return Result::Miss; } + if (!m_pos) { return Result::Miss; } const auto ke = KeyEvent(KeyEvent(input_events, input_events + num)); const auto& nextMap = m_pos->nextMap; @@ -238,7 +244,7 @@ DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], return next && ke == next->keyEvent; }); - if (find_it == nextMap.cend()) return Result::Miss; + if (find_it == nextMap.cend()) { return Result::Miss; } m_pos = (*find_it); @@ -273,7 +279,7 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) for (const auto& configItem : config) { // sanity check - if (!configItem.second.action) continue; + if (!configItem.second.action) { continue; } KeyEventItem* previous = nullptr; KeyEventItem* current = &m_rootItem; @@ -286,14 +292,14 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) return (item && item->keyEvent == keyEvent); }); + previous = current; + if (it != current->nextMap.cend()) { - previous = current; current = *it; } else { // Create new item if not found m_items.emplace_back(KeyEventItem{keyEvent}); - previous = current; current = &m_items.back(); // link previous to current previous->nextMap.push_back(current); @@ -358,10 +364,10 @@ QString NativeKeySequence::toString() const const size_t size = count(); for (size_t i = 0; i < size; ++i) { - if (i > 0) seqString += QLatin1String(", "); + if (i > 0) { seqString += QLatin1String(", "); } seqString += toString(m_keySequence[i], (i < m_nativeModifiers.size()) ? m_nativeModifiers[i] - : (uint16_t)Modifier::NoModifier); + : to_integral(Modifier::NoModifier)); } return seqString; } @@ -434,10 +440,10 @@ QString NativeKeySequence::toString(const std::vector& qtKeys, const auto size = qtKeys.size(); for (size_t i = 0; i < size; ++i) { - if (i > 0) seqString += QLatin1String(", "); + if (i > 0) { seqString += QLatin1String(", "); } seqString += toString(qtKeys[i], (i < nativeModifiers.size()) ? nativeModifiers[i] - : (uint16_t)Modifier::NoModifier); + : to_integral(Modifier::NoModifier)); } return seqString; } @@ -557,15 +563,16 @@ InputMapper::Impl::Impl(InputMapper* parent, std::shared_ptr vdev , m_vdev(std::move(vdev)) , m_seqTimer(new QTimer(parent)) { + constexpr int defaultSequenceIntervalMs = 250; m_seqTimer->setSingleShot(true); - m_seqTimer->setInterval(250); + m_seqTimer->setInterval(defaultSequenceIntervalMs); connect(m_seqTimer, &QTimer::timeout, parent, [this](){ sequenceTimeout(); }); } // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::execAction(const std::shared_ptr& action, DeviceKeyMap::Result r) { - if (!action || action->empty()) return; + if (!action || action->empty()) { return; } logDebug(input) << "Input map execAction, type =" << toString(action->type()) << ", partial_hit =" << (r == DeviceKeyMap::Result::PartialHit); @@ -594,7 +601,7 @@ void InputMapper::Impl::sequenceTimeout() if (m_lastState.first == DeviceKeyMap::Result::Valid) { // Last input event was part of a valid key sequence, but timeout hit // So we emit our stored event so far to the virtual device - if (m_vdev && m_events.size()) + if (m_vdev && !m_events.empty()) { m_vdev->emitEvents(m_events); } @@ -607,7 +614,7 @@ void InputMapper::Impl::sequenceTimeout() { execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); } - else if (m_vdev && m_events.size()) + else if (m_vdev && !m_events.empty()) { m_vdev->emitEvents(m_events); m_events.resize(0); @@ -626,15 +633,15 @@ void InputMapper::Impl::resetState() // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::emitNativeKeySequence(const NativeKeySequence& ks) { - if (!m_vdev) return; + if (!m_vdev) { return; } std::vector events; events.reserve(5); // up to 3 modifier keys + 1 key + 1 syn event for (const auto& ke : ks.nativeSequence()) { - for (const auto& ie : ke) + for (const auto& ie : ke) { events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); - + } m_vdev->emitEvents(events); events.resize(0); } @@ -660,7 +667,7 @@ InputMapper::InputMapper(std::shared_ptr virtualDevice, QObject* {} // ------------------------------------------------------------------------------------------------- -InputMapper::~InputMapper() {} +InputMapper::~InputMapper() = default; // ------------------------------------------------------------------------------------------------- std::shared_ptr InputMapper::virtualDevice() const @@ -683,13 +690,12 @@ bool InputMapper::recordingMode() const // ------------------------------------------------------------------------------------------------- void InputMapper::setRecordingMode(bool recording) { - if (impl->m_recordingMode == recording) - return; + if (impl->m_recordingMode == recording) { return; } const auto wasRecording = (impl->m_recordingMode && impl->m_seqTimer->isActive()); impl->m_recordingMode = recording; - if (wasRecording) emit recordingFinished(true); + if (wasRecording) { emit recordingFinished(true); } impl->m_seqTimer->stop(); resetState(); emit recordingModeChanged(impl->m_recordingMode); @@ -711,7 +717,7 @@ void InputMapper::setKeyEventInterval(int interval) // ------------------------------------------------------------------------------------------------- void InputMapper::addEvents(const input_event* input_events, size_t num) { - if (num == 0 || (!impl->m_vdev)) return; + if (num == 0 || (!impl->m_vdev)) { return; } // If no key mapping is configured ... if (!impl->m_recordingMode && !impl->m_keymap.hasConfig()) { @@ -723,7 +729,9 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) if (input_events[num-1].type != EV_SYN) { logWarning(input) << tr("Input mapper expects events separated by SYN event."); return; - } else if (num == 1) { + } + + if (num == 1) { logWarning(input) << tr("Ignoring single SYN event received."); return; } @@ -782,20 +790,20 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) // ------------------------------------------------------------------------------------------------- void InputMapper::addEvents(KeyEvent key_event) { - if (key_event.empty()) addEvents({}, 0); + if (key_event.empty()) { addEvents({}, 0); } - static const auto to_input_event = [](DeviceInputEvent de){ + static const auto to_input_event = [](const DeviceInputEvent& de){ struct input_event ie = {{}, de.type, de.code, de.value}; return ie; }; // If key_event do not have SYN event at end, add SYN event before sending to inputMapper. - if (key_event.back().type != EV_SYN) key_event.emplace_back(EV_SYN, SYN_REPORT, 0); + if (key_event.back().type != EV_SYN) { key_event.emplace_back(EV_SYN, SYN_REPORT, 0); } std::vector events; events.reserve(key_event.size()); - for (size_t i=0; i < key_event.size(); i++) { - events.emplace_back(to_input_event(key_event[i])); + for (const auto& dev_input_event : key_event) { + events.emplace_back(to_input_event(dev_input_event)); } addEvents(events.data(), events.size()); } @@ -809,7 +817,7 @@ void InputMapper::resetState() // ------------------------------------------------------------------------------------------------- void InputMapper::setConfiguration(const InputMapConfig& config) { - if (config == impl->m_config) return; + if (config == impl->m_config) { return; } impl->m_config = config; impl->resetState(); @@ -820,7 +828,7 @@ void InputMapper::setConfiguration(const InputMapConfig& config) // ------------------------------------------------------------------------------------------------- void InputMapper::setConfiguration(InputMapConfig&& config) { - if (config == impl->m_config) return; + if (config == impl->m_config) { return; } impl->m_config.swap(config); impl->resetState(); diff --git a/src/devicescan.cc b/src/devicescan.cc index 4b50ce75..fa88e930 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -54,16 +54,16 @@ namespace { [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); - if (it != supportedDefaultDevices.cend() && it->name.size()) return it->name; + if (it != supportedDefaultDevices.cend() && it->name.size()) { return it->name; } auto extraName = getExtraDeviceName(vendorId, productId); - if (!extraName.isEmpty()) return extraName; + if (!extraName.isEmpty()) { return extraName; } const auto ait = std::find_if(additionalDevices.cbegin(), additionalDevices.cend(), [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); - if (ait != additionalDevices.cend() && ait->name.size()) return ait->name; + if (ait != additionalDevices.cend() && ait->name.size()) { return ait->name; } return QString(); } @@ -134,11 +134,12 @@ namespace { if (property == hid_id) { const auto ids = value.split(':'); - const auto busType = ids.size() ? ids[0].toUShort(nullptr, 16) : 0; + const auto busType = ids.empty() ? 0: ids[0].toUShort(nullptr, 16); switch (busType) { case BUS_USB: spotlightDevice.id.busType = BusType::Usb; break; case BUS_BLUETOOTH: spotlightDevice.id.busType = BusType::Bluetooth; break; + default: spotlightDevice.id.busType = BusType::Unknown; } spotlightDevice.id.vendorId = ids.size() > 1 ? ids[1].toUShort(nullptr, 16) : 0; spotlightDevice.id.productId = ids.size() > 2 ? ids[2].toUShort(nullptr, 16) : 0; @@ -156,7 +157,7 @@ namespace { } return spotlightDevice; } -} +} // end anonymous namespace namespace DeviceScan { // ----------------------------------------------------------------------------------------------- @@ -183,15 +184,15 @@ namespace DeviceScan { hidIt.next(); const QFileInfo uEventFile(QDir(hidIt.filePath()).filePath("uevent")); - if (!uEventFile.exists()) continue; + if (!uEventFile.exists()) { continue; } // Get basic information from uevent file Device newDevice = deviceFromUEventFile(uEventFile.filePath()); const auto& deviceId = newDevice.id; // Skip unsupported devices - if (deviceId.vendorId == 0 || deviceId.productId == 0) continue; + if (deviceId.vendorId == 0 || deviceId.productId == 0) { continue; } if (!isDeviceSupported(deviceId.vendorId, deviceId.productId) - && !(isAdditionallySupported(deviceId.vendorId, deviceId.productId, additionalDevices))) continue; + && !(isAdditionallySupported(deviceId.vendorId, deviceId.productId, additionalDevices))) { continue; } // Check if device is already in list (and we have another sub-device for it) const auto find_it = std::find_if(result.devices.begin(), result.devices.end(), @@ -226,7 +227,7 @@ namespace DeviceScan { while (dirIt.hasNext()) { dirIt.next(); - if (!dirIt.fileName().startsWith("event")) continue; + if (!dirIt.fileName().startsWith("event")) { continue; } subDevice.type = SubDevice::Type::Event; subDevice.deviceFile = readPropertyFromDeviceFile(QDir(dirIt.filePath()).filePath("uevent"), "DEVNAME"); if (!subDevice.deviceFile.isEmpty()) { @@ -235,7 +236,7 @@ namespace DeviceScan { } } - if (subDevice.deviceFile.isEmpty()) continue; + if (subDevice.deviceFile.isEmpty()) { continue; } subDevice.phys = readStringFromDeviceFile(QDir(inputIt.filePath()).filePath("phys")); ++eventSubDeviceCount; @@ -260,7 +261,7 @@ namespace DeviceScan { // Spotlight (Bluetooth) have hidraw interface in the same folder. However // for other connection, it has separate folder for hidraw device and input device. - if (!(rootDevice.id.busType == BusType::Bluetooth) && eventSubDeviceCount > 0) continue; + if (!(rootDevice.id.busType == BusType::Bluetooth) && eventSubDeviceCount > 0) { continue; } // Iterate over 'hidraw' sub-dircectory, check for hidraw device node const QFileInfo hidrawSubdir(QDir(hidIt.filePath()).filePath("hidraw")); @@ -270,13 +271,13 @@ namespace DeviceScan { while (hidrawIt.hasNext()) { hidrawIt.next(); - if (!hidrawIt.fileName().startsWith("hidraw")) continue; + if (!hidrawIt.fileName().startsWith("hidraw")) { continue; } SubDevice subDevice; subDevice.deviceFile = readPropertyFromDeviceFile(QDir(hidrawIt.filePath()).filePath("uevent"), "DEVNAME"); if (!subDevice.deviceFile.isEmpty()) { subDevice.type = SubDevice::Type::Hidraw; subDevice.deviceFile = QDir("/dev").filePath(subDevice.deviceFile); - if (subDevice.deviceFile.isEmpty()) continue; + if (subDevice.deviceFile.isEmpty()) { continue; } const QFileInfo fi(subDevice.deviceFile); subDevice.deviceReadable = fi.isReadable(); subDevice.deviceWritable = fi.isWritable(); @@ -305,4 +306,4 @@ namespace DeviceScan { return result; } -} +} // end namespace DeviceScan diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 9e924323..521e76ea 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -20,9 +20,9 @@ #include #include #include -#include #include #include +#include DECLARE_LOGGING_CATEGORY(preferences) @@ -45,7 +45,7 @@ namespace { } return false; } -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- DevicesWidget::DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent) @@ -69,10 +69,11 @@ DevicesWidget::DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* } // ------------------------------------------------------------------------------------------------- -const DeviceId DevicesWidget::currentDeviceId() const +DeviceId DevicesWidget::currentDeviceId() const { - if (m_devicesCombo->currentIndex() < 0) + if (m_devicesCombo->currentIndex() < 0) { return invalidDeviceId; + } return qvariant_cast(m_devicesCombo->currentData()); } @@ -171,7 +172,7 @@ QWidget* DevicesWidget::createInputMapperWidget(Settings* settings, Spotlight* / const auto tblView = new InputMapConfigView(imWidget); const auto imModel = new InputMapConfigModel(m_inputMapper, imWidget); - if (m_inputMapper) imModel->setConfiguration(m_inputMapper->configuration()); + if (m_inputMapper) { imModel->setConfiguration(m_inputMapper->configuration()); } tblView->setModel(imModel); const auto selectionModel = tblView->selectionModel(); @@ -339,7 +340,7 @@ void DevicesWidget::updateTimerTab(Spotlight* spotlight) auto getVibrateConnection = [](const std::shared_ptr& conn) { if (conn) { for (const auto& item : conn->subDevices()) { - if (item.second->hasFlags(DeviceFlag::Vibrate)) return item.second; + if (item.second->hasFlags(DeviceFlag::Vibrate)) { return item.second; } } } return std::shared_ptr{}; @@ -348,7 +349,7 @@ void DevicesWidget::updateTimerTab(Spotlight* spotlight) const auto currentConn = spotlight->deviceConnection(currentDeviceId()); const auto vibrateConn = getVibrateConnection(currentConn); - if (m_timerTabContext) m_timerTabContext->deleteLater(); + if (m_timerTabContext) { m_timerTabContext->deleteLater(); } if (vibrateConn) { @@ -365,8 +366,8 @@ void DevicesWidget::updateTimerTab(Spotlight* spotlight) if (currentConn) { m_timerTabContext = QPointer(new QObject(this)); connect(&*currentConn, &DeviceConnection::subDeviceFlagsChanged, m_timerTabContext, - [currId=currentDeviceId(), spotlight, this](const DeviceId& id, const QString&) { - if (currId != id) return; + [currId=currentDeviceId(), spotlight, this](const DeviceId& id, const QString& /* path */) { + if (currId != id) { return; } updateTimerTab(spotlight); }); } @@ -413,8 +414,9 @@ DeviceInfoWidget::DeviceInfoWidget(QWidget* parent) const auto layout = new QVBoxLayout(this); layout->addWidget(m_textEdit); + constexpr int delayedUpdateTimerInterval = 150; m_delayedUpdateTimer->setSingleShot(true); - m_delayedUpdateTimer->setInterval(150); + m_delayedUpdateTimer->setInterval(delayedUpdateTimerInterval); connect(m_delayedUpdateTimer, &QTimer::timeout, this, &DeviceInfoWidget::updateTextEdit); m_batteryInfoTimer->setSingleShot(false); @@ -430,7 +432,7 @@ void DeviceInfoWidget::delayedTextEditUpdate() { // ------------------------------------------------------------------------------------------------- void DeviceInfoWidget::setDeviceConnection(DeviceConnection* connection) { - if (m_connection == connection) return; + if (m_connection == connection) { return; } if (m_connectionContext) { m_connectionContext->deleteLater(); } m_connection = connection; @@ -453,7 +455,7 @@ void DeviceInfoWidget::setDeviceConnection(DeviceConnection* connection) m_deviceBaseInfo.emplace_back("Bus Type", toString(m_connection->deviceId().busType, false)); connect(m_connection, &DeviceConnection::subDeviceConnected, m_connectionContext, - [this](const DeviceId&, const QString& path) + [this](const DeviceId& /* deviceId */, const QString& path) { if (const auto sdc = m_connection->subDevice(path)) { @@ -464,7 +466,7 @@ void DeviceInfoWidget::setDeviceConnection(DeviceConnection* connection) }); connect(m_connection, &DeviceConnection::subDeviceConnected, m_connectionContext, - [this](const DeviceId&, const QString& path) + [this](const DeviceId& /* deviceId */, const QString& path) { const auto it = m_subDevices.find(path); if (it == m_subDevices.cend()) { @@ -681,7 +683,7 @@ void DeviceInfoWidget::initSubdeviceInfo() for (const auto& sd : m_connection->subDevices()) { const auto& sdc = sd.second; - if (sdc->path().isEmpty()) continue; + if (sdc->path().isEmpty()) { continue; } updateSubdeviceInfo(sdc.get()); connectToSubdeviceUpdates(sdc.get()); @@ -717,7 +719,7 @@ void DeviceInfoWidget::updateHidppInfo(SubHidppConnection* hdc) , DeviceFlag::BackHold , DeviceFlag::PointerSpeed }) { - if (hdc->hasFlags(flag)) m_hidppInfo.hidppFlags.push_back(toString(flag, false)); + if (hdc->hasFlags(flag)) { m_hidppInfo.hidppFlags.push_back(toString(flag, false)); } } } diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 7aec6549..cde9dc1d 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -31,7 +31,7 @@ class DevicesWidget : public QWidget public: explicit DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent = nullptr); - const DeviceId currentDeviceId() const; + DeviceId currentDeviceId() const; signals: void currentDeviceChanged(const DeviceId&); diff --git a/src/extra-devices.cc.in b/src/extra-devices.cc.in index cbe95713..6f961ac9 100644 --- a/src/extra-devices.cc.in +++ b/src/extra-devices.cc.in @@ -13,7 +13,7 @@ namespace { // List of supported extra-devices const std::vector supportedExtraDevices { // @SUPPORTED_EXTRA_DEVICES@ }; -} +} // end anonymous namespace // Function declaration to check for extra devices, definition in generated source bool isExtraDeviceSupported(quint16 vendorId, quint16 productId) diff --git a/src/hidpp.cc b/src/hidpp.cc index 2c497f3e..04612e91 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -607,7 +607,7 @@ FirmwareInfo::FirmwareType FirmwareInfo::firmwareType() const // ------------------------------------------------------------------------------------------------- QString FirmwareInfo::firmwarePrefix() const { - if (!m_rawMsg.isLong()) return QString(); + if (!m_rawMsg.isLong()) { return QString(); } return QString( QByteArray::fromRawData(reinterpret_cast(&m_rawMsg[Offset::FwPrefix]), 3) @@ -617,7 +617,7 @@ QString FirmwareInfo::firmwarePrefix() const // ------------------------------------------------------------------------------------------------- uint16_t FirmwareInfo::firmwareVersion() const { - if (!m_rawMsg.isLong()) return 0; + if (!m_rawMsg.isLong()) { return 0; } const auto& fwVersionMsb = m_rawMsg[Offset::FwVersion]; const auto& fwVersionLsb = m_rawMsg[Offset::FwVersion+1]; @@ -632,7 +632,7 @@ uint16_t FirmwareInfo::firmwareVersion() const // ------------------------------------------------------------------------------------------------- uint16_t FirmwareInfo::firmwareBuild() const { - if (!m_rawMsg.isLong()) return 0; + if (!m_rawMsg.isLong()) { return 0; } const auto& fwBuildMsb = m_rawMsg[Offset::FwBuild]; const auto& fwBuildLsb = m_rawMsg[Offset::FwBuild+1]; diff --git a/src/iconwidgets.cc b/src/iconwidgets.cc index bffeb31d..d789bd94 100644 --- a/src/iconwidgets.cc +++ b/src/iconwidgets.cc @@ -12,7 +12,7 @@ namespace { bool isDark(const QColor& c) { return !isLight(c); } constexpr int defaultIconLabelSize = 32; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- IconButton::IconButton(Font::Icon symbol, QWidget* parent) diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index c5a4c15b..6aabe447 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -13,7 +13,7 @@ // ------------------------------------------------------------------------------------------------- namespace { const InputMapModelItem invalidItem_; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- InputMapConfigModel::InputMapConfigModel(QObject* parent) @@ -41,10 +41,9 @@ int InputMapConfigModel::columnCount(const QModelIndex& /*parent*/) const // ------------------------------------------------------------------------------------------------- Qt::ItemFlags InputMapConfigModel::flags(const QModelIndex &index) const { - if (index.column() == InputSeqCol) - return (QAbstractTableModel::flags(index) | Qt::ItemIsEditable); - else if (index.column() == ActionCol) + if (index.column() == InputSeqCol || index.column() == ActionCol) { return (QAbstractTableModel::flags(index) | Qt::ItemIsEditable); + } return QAbstractTableModel::flags(index) & ~Qt::ItemIsEditable; } @@ -68,6 +67,7 @@ QVariant InputMapConfigModel::headerData(int section, Qt::Orientation orientatio case InputSeqCol: return tr("Input Sequence"); case ActionTypeCol: return "Type"; case ActionCol: return tr("Mapped Action"); + default: return "Invalid"; } } else if (orientation == Qt::Vertical) @@ -85,8 +85,9 @@ QVariant InputMapConfigModel::headerData(int section, Qt::Orientation orientatio // ------------------------------------------------------------------------------------------------- const InputMapModelItem& InputMapConfigModel::configData(const QModelIndex& index) const { - if (index.row() >= static_cast(m_configItems.size())) + if (index.row() >= static_cast(m_configItems.size())) { return invalidItem_; + } return m_configItems[index.row()]; } @@ -94,7 +95,7 @@ const InputMapModelItem& InputMapConfigModel::configData(const QModelIndex& inde // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::removeConfigItemRows(int fromRow, int toRow) { - if (fromRow > toRow) return; + if (fromRow > toRow) { return; } beginRemoveRows(QModelIndex(), fromRow, toRow); for (int i = toRow; i >= fromRow && i < m_configItems.size(); --i) { @@ -107,7 +108,7 @@ void InputMapConfigModel::removeConfigItemRows(int fromRow, int toRow) // ------------------------------------------------------------------------------------------------- int InputMapConfigModel::addNewItem(std::shared_ptr action) { - if (!action) return -1; + if (!action) { return -1; } const auto row = m_configItems.size(); beginInsertRows(QModelIndex(), row, row); @@ -128,7 +129,7 @@ void InputMapConfigModel::configureInputMapper() // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::removeConfigItemRows(std::vector rows) { - if (rows.empty()) return; + if (rows.empty()) { return; } std::sort(rows.rbegin(), rows.rend()); int seq_last = rows.front(); @@ -212,9 +213,9 @@ void InputMapConfigModel::setKeySequence(const QModelIndex& index, const NativeK // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setItemActionType(const QModelIndex& idx, Action::Type type) { - if (idx.row() >= m_configItems.size()) return; + if (idx.row() >= m_configItems.size()) { return; } auto& item = m_configItems[idx.row()]; - if (item.action->type() == type) return; + if (item.action->type() == type) { return; } switch(type) { @@ -265,7 +266,7 @@ InputMapConfig InputMapConfigModel::configuration() const for (const auto& item : m_configItems) { - if (item.deviceSequence.size() == 0) continue; + if (item.deviceSequence.empty()) { continue; } config.emplace(item.deviceSequence, MappedAction{item.action}); } @@ -330,7 +331,7 @@ InputMapConfigView::InputMapConfigView(QWidget* parent) [this, imSeqDelegate, actionDelegate](const QPoint& pos) { const auto idx = indexAt(pos); - if (!idx.isValid()) return; + if (!idx.isValid()) { return; } switch(idx.column()) { @@ -350,7 +351,7 @@ InputMapConfigView::InputMapConfigView(QWidget* parent) connect(this, &QTableView::doubleClicked, this, [this](const QModelIndex& idx) { - if (!idx.isValid()) return; + if (!idx.isValid()) { return; } if (idx.column() == InputMapConfigModel::ActionTypeCol) { const auto pos = viewport()->mapToGlobal(visualRect(currentIndex()).bottomLeft()); m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), @@ -389,15 +390,16 @@ void InputMapConfigView::keyPressEvent(QKeyEvent* e) } break; case Qt::Key_Delete: - if (const auto imModel = qobject_cast(model())) - switch (currentIndex().column()) - { - case InputMapConfigModel::InputSeqCol: - imModel->setInputSequence(currentIndex(), KeyEventSequence{}); - return; - case InputMapConfigModel::ActionCol: - imModel->setKeySequence(currentIndex(), NativeKeySequence()); - return; + if (const auto imModel = qobject_cast(model())) { + switch (currentIndex().column()) + { + case InputMapConfigModel::InputSeqCol: + imModel->setInputSequence(currentIndex(), KeyEventSequence{}); + return; + case InputMapConfigModel::ActionCol: + imModel->setKeySequence(currentIndex(), NativeKeySequence()); + return; + } } break; case Qt::Key_Tab: diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index d13a6643..24dd2b6e 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -29,7 +29,7 @@ namespace { return std::equal(first.cbegin(), first.cend(), second.cbegin(), second.cend(), [](const DeviceInputEvent& e1, const DeviceInputEvent& e2) { - if (e1.type != EV_KEY) return e1 == e2; // just compare for non key events + if (e1.type != EV_KEY) { return e1 == e2; } // just compare for non key events return (e2.type == EV_KEY // special handling for key events... && e1.code == e2.code @@ -42,7 +42,7 @@ namespace { int drawKeyEvent(int startX, QPainter& p, const QStyleOption& option, const KeyEvent& ke, bool buttonTap = false) { - if (ke.empty()) return 0; + if (ke.empty()) { return 0; } static auto const pressChar = QChar(0x2193); // ↓ static auto const releaseChar = QChar(0x2191); // ↑ @@ -60,10 +60,11 @@ namespace { p.save(); - if (option.state & QStyle::State_Selected) + if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::HighlightedText)); - else + } else { p.setPen(option.palette.color(QPalette::Text)); + } QRect br; p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); @@ -97,8 +98,8 @@ namespace { const int paddingX = static_cast(QStaticText(" ").size().width()); for (auto it = kes.cbegin(); it!=kes.cend(); ++it) { - if (it != kes.cbegin()) sequenceWidth += paddingX; - if (startX + sequenceWidth >= option.rect.width()) break; + if (it != kes.cbegin()) { sequenceWidth += paddingX; } + if (startX + sequenceWidth >= option.rect.width()) { break; } const bool isTap = [&]() { // Check if this event and the next event represent a button press & release @@ -137,7 +138,7 @@ namespace { return br.width(); } -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- InputSeqEdit::InputSeqEdit(QWidget* parent) @@ -157,9 +158,7 @@ InputSeqEdit::InputSeqEdit(InputMapper* im, QWidget* parent) } // ------------------------------------------------------------------------------------------------- -InputSeqEdit::~InputSeqEdit() -{ -} +InputSeqEdit::~InputSeqEdit() = default; // ------------------------------------------------------------------------------------------------- QStyleOptionFrame InputSeqEdit::styleOption() const @@ -195,7 +194,7 @@ QSize InputSeqEdit::sizeHint() const } // ------------------------------------------------------------------------------------------------- -void InputSeqEdit::paintEvent(QPaintEvent*) +void InputSeqEdit::paintEvent(QPaintEvent* /* paintEvent */) { const QStyleOptionFrame option = styleOption(); @@ -205,21 +204,19 @@ void InputSeqEdit::paintEvent(QPaintEvent*) const bool recording = m_inputMapper && m_inputMapper->recordingMode(); const auto& fm = option.fontMetrics; - int xPos = (option.rect.height()-fm.height()) / 2; + const int xPos = (option.rect.height()-fm.height()) / 2; if (recording) { - const auto spacingX = QStaticText(" ").size().width(); - xPos += drawRecordingSymbol(xPos, p, option) + spacingX; + drawRecordingSymbol(xPos, p, option); if (m_recordedSequence.empty()) { - xPos += drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")) + spacingX; + drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")); } else { - xPos += drawKeyEventSequence(xPos, p, option, m_recordedSequence, false); + drawKeyEventSequence(xPos, p, option, m_recordedSequence, false); } } - else - { - xPos += drawKeyEventSequence(xPos, p, option, m_inputSequence); + else { + drawKeyEventSequence(xPos, p, option, m_inputSequence); } } @@ -232,7 +229,7 @@ const KeyEventSequence& InputSeqEdit::inputSequence() const // ------------------------------------------------------------------------------------------------- void InputSeqEdit::setInputSequence(const KeyEventSequence& is) { - if (is == m_inputSequence) return; + if (is == m_inputSequence) { return; } m_inputSequence = is; update(); @@ -242,7 +239,7 @@ void InputSeqEdit::setInputSequence(const KeyEventSequence& is) // ------------------------------------------------------------------------------------------------- void InputSeqEdit::clear() { - if (m_inputSequence.size() == 0) return; + if (m_inputSequence.empty()) { return; } m_inputSequence.clear(); update(); @@ -253,7 +250,7 @@ void InputSeqEdit::clear() void InputSeqEdit::mouseDoubleClickEvent(QMouseEvent* e) { QWidget::mouseDoubleClickEvent(e); - if (!m_inputMapper) return; + if (!m_inputMapper) { return; } e->accept(); m_inputMapper->setRecordingMode(!m_inputMapper->recordingMode()); } @@ -266,7 +263,8 @@ void InputSeqEdit::keyPressEvent(QKeyEvent* e) m_inputMapper->setRecordingMode(!m_inputMapper->recordingMode()); return; } - else if (e->key() == Qt::Key_Escape) + + if (e->key() == Qt::Key_Escape) { if (m_inputMapper && m_inputMapper->recordingMode()) { m_inputMapper->setRecordingMode(false); @@ -275,10 +273,11 @@ void InputSeqEdit::keyPressEvent(QKeyEvent* e) } else if (e->key() == Qt::Key_Delete) { - if (m_inputMapper && m_inputMapper->recordingMode()) + if (m_inputMapper && m_inputMapper->recordingMode()) { m_inputMapper->setRecordingMode(false); - else + } else { setInputSequence(KeyEventSequence{}); + } return; } @@ -294,8 +293,9 @@ void InputSeqEdit::keyReleaseEvent(QKeyEvent* e) // ------------------------------------------------------------------------------------------------- void InputSeqEdit::focusOutEvent(QFocusEvent* e) { - if (m_inputMapper) + if (m_inputMapper) { m_inputMapper->setRecordingMode(false); + } QWidget::focusOutEvent(e); } @@ -303,7 +303,7 @@ void InputSeqEdit::focusOutEvent(QFocusEvent* e) // ------------------------------------------------------------------------------------------------- void InputSeqEdit::setInputMapper(InputMapper* im) { - if (m_inputMapper == im) return; + if (m_inputMapper == im) { return; } auto removeIm = [this](){ if (m_inputMapper) { @@ -315,7 +315,7 @@ void InputSeqEdit::setInputMapper(InputMapper* im) removeIm(); m_inputMapper = im; - if (m_inputMapper == nullptr) return; + if (m_inputMapper == nullptr) { return; } connect(m_inputMapper, &InputMapper::destroyed, this, [removeIm=std::move(removeIm)](){ @@ -327,14 +327,14 @@ void InputSeqEdit::setInputMapper(InputMapper* im) }); connect(m_inputMapper, &InputMapper::recordingFinished, this, [this](bool canceled){ - if (!canceled) setInputSequence(m_recordedSequence); + if (!canceled) { setInputSequence(m_recordedSequence); } m_inputMapper->setRecordingMode(false); m_recordedSequence.clear(); }); connect(m_inputMapper, &InputMapper::recordingModeChanged, this, [this](bool recording){ update(); - if (!recording) emit editingFinished(this); + if (!recording) { emit editingFinished(this); } }); connect(m_inputMapper, &InputMapper::keyEventRecorded, this, [this](const KeyEvent& ke){ @@ -376,13 +376,14 @@ int InputSeqEdit::drawEmptyIndicator(int startX, QPainter& p, const QStyleOption { p.save(); p.setFont([&p](){ auto f = p.font(); f.setItalic(true); return f; }()); - if (option.state & QStyle::State_Selected) + if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::Disabled, QPalette::HighlightedText)); - else + } else { p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); + } static const QStaticText textNone(InputSeqEdit::tr("None")); - const auto top = (option.rect.height() - textNone.size().height()) / 2; + const auto top = static_cast((option.rect.height() - textNone.size().height()) / 2); p.drawStaticText(startX + option.rect.left(), option.rect.top() + top, textNone); p.restore(); return static_cast(textNone.size().width()); @@ -453,10 +454,10 @@ QWidget* InputSeqDelegate::createEditor(QWidget* parent, { if (const auto imModel = qobject_cast(index.model())) { - if (imModel->inputMapper()) imModel->inputMapper()->setRecordingMode(false); + if (imModel->inputMapper()) { imModel->inputMapper()->setRecordingMode(false); } auto *editor = new InputSeqEdit(imModel->inputMapper(), parent); connect(editor, &InputSeqEdit::editingFinished, this, &InputSeqDelegate::commitAndCloseEditor); - if (imModel->inputMapper()) imModel->inputMapper()->setRecordingMode(true); + if (imModel->inputMapper()) { imModel->inputMapper()->setRecordingMode(true); } return editor; } @@ -517,12 +518,12 @@ QSize InputSeqDelegate::sizeHint(const QStyleOptionViewItem& option, void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos) { - if (!index.isValid() || !model) return; + if (!index.isValid() || !model) { return; } const auto& specialInputs = model->inputMapper()->specialInputs(); if (!specialInputs.empty()) { - QMenu* menu = new QMenu(parent); + auto* const menu = new QMenu(parent); for (const auto& button : specialInputs) { const auto qaction = menu->addAction(button.name); diff --git a/src/linuxdesktop.cc b/src/linuxdesktop.cc index 2a0f588d..0e2efc44 100644 --- a/src/linuxdesktop.cc +++ b/src/linuxdesktop.cc @@ -105,11 +105,13 @@ LinuxDesktop::LinuxDesktop(QObject* parent) QPixmap LinuxDesktop::grabScreen(QScreen* screen) const { - if (screen == nullptr) + if (screen == nullptr) { return QPixmap(); + } - if (isWayland()) + if (isWayland()) { return grabScreenWayland(screen); + } #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) const bool isVirtualDesktop = QApplication::primaryScreen()->virtualSiblings().size() > 1; @@ -117,8 +119,9 @@ QPixmap LinuxDesktop::grabScreen(QScreen* screen) const const bool isVirtualDesktop = QApplication::desktop()->isVirtualDesktop(); #endif - if (isVirtualDesktop) + if (isVirtualDesktop) { return grabScreenVirtualDesktop(screen); + } // everything else.. usually X11 return screen->grabWindow(0); diff --git a/src/logging.cc b/src/logging.cc index d3f86aec..9a77b08e 100644 --- a/src/logging.cc +++ b/src/logging.cc @@ -121,10 +121,11 @@ namespace { const auto logMsg = QString("[%1][%2][%3] %4").arg(QDateTime::currentDateTime().toString(dateFormat), typeToShortString(type), category, msgQString); - if (type == QtDebugMsg || type == QtInfoMsg) + if (type == QtDebugMsg || type == QtInfoMsg) { std::cout << qUtf8Printable(logMsg) << std::endl; - else + } else { std::cerr << qUtf8Printable(logMsg) << std::endl; + } logToTextEdit(logMsg); } @@ -134,7 +135,7 @@ namespace logging { void registerTextEdit(QPlainTextEdit* textEdit) { logPlainTextEdit = textEdit; - if (!logPlainTextEdit) return; + if (!logPlainTextEdit) { return; } const auto index = logPlainTextEdit->metaObject()->indexOfMethod("appendPlainText(QString)"); logAppendMetaMethod = logPlainTextEdit->metaObject()->method(index); @@ -162,20 +163,20 @@ namespace logging { level levelFromName(const QString& name) { const auto lvlName = name.toLower(); - if (lvlName == "dbg" || lvlName == "debug") return level::debug; - if (lvlName == "inf" || lvlName == "info") return level::info; - if (lvlName == "wrn" || lvlName == "warning") return level::warning; - if (lvlName == "err" || lvlName == "error") return level::error; + if (lvlName == "dbg" || lvlName == "debug") { return level::debug; } + if (lvlName == "inf" || lvlName == "info") { return level::info; } + if (lvlName == "wrn" || lvlName == "warning") { return level::warning; } + if (lvlName == "err" || lvlName == "error") { return level::error; } return level::unknown; } level currentLevel() { - if (currentCategoryFilter == defaultCategoryFilter) return level::custom; - if (currentCategoryFilter == categoryFilterDebug) return level::debug; - if (currentCategoryFilter == categoryFilterInfo) return level::info; - if (currentCategoryFilter == categoryFilterWarning) return level::warning; - if (currentCategoryFilter == categoryFilterError) return level::error; + if (currentCategoryFilter == defaultCategoryFilter) { return level::custom; } + if (currentCategoryFilter == categoryFilterDebug) { return level::debug; } + if (currentCategoryFilter == categoryFilterInfo) { return level::info; } + if (currentCategoryFilter == categoryFilterWarning) { return level::warning; } + if (currentCategoryFilter == categoryFilterError) { return level::error; } return level::unknown; } @@ -183,16 +184,17 @@ namespace logging { { QLoggingCategory::CategoryFilter newFilter = currentCategoryFilter; - if (lvl == level::debug) + if (lvl == level::debug) { newFilter = categoryFilterDebug; - else if (lvl == level::info) + } else if (lvl == level::info) { newFilter = categoryFilterInfo; - else if (lvl == level::warning) + } else if (lvl == level::warning) { newFilter = categoryFilterWarning; - else if (lvl == level::error) + } else if (lvl == level::error) { newFilter = categoryFilterError; - else if (lvl == level::custom) + } else if (lvl == level::custom) { newFilter = defaultCategoryFilter; + } if (newFilter != currentCategoryFilter) { QLoggingCategory::installFilter(newFilter); @@ -200,7 +202,7 @@ namespace logging { } } - QString hexId(unsigned short id) { + QString hexId(uint16_t id) { return QString("%1").arg(id, 4, 16, QChar('0')); } -} +} // end namespace logging diff --git a/src/logging.h b/src/logging.h index e9552320..b849c734 100644 --- a/src/logging.h +++ b/src/logging.h @@ -70,7 +70,7 @@ namespace logging { void registerTextEdit(QPlainTextEdit* textEdit); - QString hexId(unsigned short id); + QString hexId(uint16_t id); } diff --git a/src/main.cc b/src/main.cc index 7775c7aa..58ac0593 100644 --- a/src/main.cc +++ b/src/main.cc @@ -15,8 +15,8 @@ #endif #include -#include #include +#include #define XSTRINGIFY(s) STRINGIFY(s) #define STRINGIFY(x) #x @@ -24,6 +24,12 @@ LOGGING_CATEGORY(appMain, "main") namespace { + // ----------------------------------------------------------------------------------------------- + constexpr int PROJECTEUR_ERROR_ANOTHER_INST_RUNNING = 42; + constexpr int PROJECTEUR_ERROR_NO_INSTANCE_FOUND = 43; + constexpr int PROJECTEUR_ERROR_EMPTY_COMMAND_PROPS = 44; + + // ----------------------------------------------------------------------------------------------- class Main : public QObject {}; std::ostream& operator<<(std::ostream& os, const QString& s) { @@ -47,118 +53,253 @@ namespace { { if (sig == SIGINT) { print() << "..."; - if (qApp) QCoreApplication::quit(); + if (qApp) { QCoreApplication::quit(); } } } -} -int main(int argc, char *argv[]) -{ - QCoreApplication::setApplicationName("Projecteur"); - QCoreApplication::setApplicationVersion(projecteur::version_string()); - ProjecteurApplication::Options options; - QStringList ipcCommands; + // ----------------------------------------------------------------------------------------------- + // Helper function to get the range of valid values for a string property + QString getValuesDescription(const Settings::StringProperty& sp) + { + if (sp.type == Settings::StringProperty::Type::Integer + || sp.type == Settings::StringProperty::Type::Double) { + return QString("(%1 ... %2)").arg(sp.range[0].toString(), sp.range[1].toString()); + } + + if (sp.type == Settings::StringProperty::Type::Bool) { + return "(false, true)"; + } + + if (sp.type == Settings::StringProperty::Type::Color) { + return "(HTML-color; #RRGGBB)"; + } + + if (sp.type == Settings::StringProperty::Type::StringEnum) { + QStringList values; + for (const auto& v : sp.range) { + values.push_back(v.toString()); + } + return QString("(%1)").arg(values.join(", ")); + } + return QString(); + } + + // ----------------------------------------------------------------------------------------------- + void printVersionInfo(const ProjecteurApplication::Options& options, bool fullVersionOption) + { + print() << QCoreApplication::applicationName().toStdString() << " " + << projecteur::version_string(); + + if (fullVersionOption || + (std::string(projecteur::version_branch()) != "master" && + std::string(projecteur::version_branch()) != "not-within-git-repo")) + { // Not a build from master branch, print out additional information: + print() << " - git-branch: " << projecteur::version_branch(); + print() << " - git-hash: " << projecteur::version_fullhash(); + } + + // Show if we have a build from modified sources + if (projecteur::version_isdirty()) { + print() << " - dirty-flag: " << projecteur::version_isdirty(); + } + + // Additional useful information + if (fullVersionOption) + { + print() << " - compiler: " << XSTRINGIFY(CXX_COMPILER_ID) << " " + << XSTRINGIFY(CXX_COMPILER_VERSION); + print() << " - build-type: " << projecteur::version_buildtype(); + print() << " - qt-version: (build: " << QT_VERSION_STR << ", runtime: " << qVersion() << ")"; + + const auto result = DeviceScan::getDevices(options.additionalDevices); + print() << " - device-scan: " + << QString("(errors: %1, devices: %2 [readable: %3, writable: %4])") + .arg(result.errorMessages.size()).arg(result.devices.size()) + .arg(result.numDevicesReadable).arg(result.numDevicesWritable); + } + } + + // ----------------------------------------------------------------------------------------------- + void printDeviceInfo(const ProjecteurApplication::Options& options) + { + const auto result = DeviceScan::getDevices(options.additionalDevices); + print() << QCoreApplication::applicationName() << " " + << projecteur::version_string() << "; " << Main::tr("device scan") << std::endl; + + for (const auto& errmsg : result.errorMessages) { + print() << "** " << Main::tr("Error: ") << errmsg; + } + + print() << (!result.errorMessages.empty() ? "\n" : "") + << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") + .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); + + for (const auto& device : result.devices) + { + print() << "\n" + << " +++ " << "name: '" << device.name << "'"; + if (!device.userName.isEmpty()) { + print() << " " << "userName: '" << device.userName << "'"; + } + + const QStringList subDeviceList = [&device](){ + QStringList subDeviceList; + for (const auto& sd: device.subDevices) { + if (sd.deviceFile.size()) { subDeviceList.push_back(sd.deviceFile); } + } + return subDeviceList; + }(); + + const bool allReadable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), + [](const auto& subDevice){ + return subDevice.deviceReadable; + }); + + const bool allWriteable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), + [](const auto& subDevice){ + return subDevice.deviceWritable; + }); + + print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); + print() << " " << "productId: " << logging::hexId(device.id.productId); + print() << " " << "phys: " << device.id.phys; + print() << " " << "busType: " << toString(device.id.busType); + print() << " " << "devices: " << subDeviceList.join(", "); + print() << " " << "readable: " << (allReadable ? "true" : "false"); + print() << " " << "writable: " << (allWriteable ? "true" : "false"); + } + } + + // ----------------------------------------------------------------------------------------------- + void addDevices(ProjecteurApplication::Options& options, const QStringList& devices) + { + for (auto& deviceValue : devices) { + const auto devAttribs = deviceValue.split(":"); + const uint16_t vendorId = devAttribs.size() > 0 ? devAttribs[0].toUShort(nullptr, 16) : 0; + const uint16_t productId = devAttribs.size() > 1 ? devAttribs[1].toUShort(nullptr, 16) : 0; + if (vendorId == 0 || productId == 0) { + error() << Main::tr("Invalid vendor/productId pair: ") << deviceValue; + } else { + const QString name = (devAttribs.size() >= 3) ? devAttribs[2] : ""; + options.additionalDevices.push_back({vendorId, productId, false, name}); + } + } + } + + // ----------------------------------------------------------------------------------------------- + struct ProjecteurCmdLineParser { QCommandLineParser parser; - parser.setApplicationDescription(Main::tr("Linux/X11 application for the Logitech Spotlight device.")); - const QCommandLineOption versionOption(QStringList{ "v", "version"}, Main::tr("Print application version.")); - const QCommandLineOption fullVersionOption(QStringList{ "f", "fullversion" }); - const QCommandLineOption helpOption(QStringList{ "h", "help"}, Main::tr("Show command line usage.")); - const QCommandLineOption fullHelpOption(QStringList{ "help-all"}, Main::tr("Show complete command line usage with all properties.")); - const QCommandLineOption cfgFileOption(QStringList{ "cfg" }, Main::tr("Set custom config file."), "file"); - const QCommandLineOption commandOption(QStringList{ "c", "command"}, Main::tr("Send command/property to a running instance."), "cmd"); - const QCommandLineOption deviceInfoOption(QStringList{ "d", "device-scan"}, Main::tr("Print device-scan results.")); - const QCommandLineOption logLvlOption(QStringList{ "l", "log-level" }, Main::tr("Set log level (dbg,inf,wrn,err)."), "lvl"); - const QCommandLineOption disableUInputOption(QStringList{ "disable-uinput" }, Main::tr("Disable uinput support.")); - const QCommandLineOption showDlgOnStartOption(QStringList{ "show-dialog" }, Main::tr("Show preferences dialog on start.")); - const QCommandLineOption dialogMinOnlyOption(QStringList{ "m", "minimize-only" }, Main::tr("Only allow minimizing the dialog.")); - const QCommandLineOption disableOverlayOption(QStringList{ "disable-overlay" }, Main::tr("Disable spotlight overlay completely.")); - const QCommandLineOption additionalDeviceOption(QStringList{ "D", "additional-device"}, + // parser.setApplicationDescription(Main::tr("Linux/X11 application for the Logitech Spotlight device.")); + const QCommandLineOption versionOption_ = {QStringList{ "v", "version"}, Main::tr("Print application version.")}; + const QCommandLineOption fullVersionOption_ = QCommandLineOption{QStringList{ "f", "fullversion" }}; + const QCommandLineOption helpOption_ = {QStringList{ "h", "help"}, Main::tr("Show command line usage.")}; + const QCommandLineOption fullHelpOption_ = {QStringList{ "help-all"}, Main::tr("Show complete command line usage with all properties.")}; + const QCommandLineOption cfgFileOption_ = {QStringList{ "cfg" }, Main::tr("Set custom config file."), "file"}; + const QCommandLineOption commandOption_ = {QStringList{ "c", "command"}, Main::tr("Send command/property to a running instance."), "cmd"}; + const QCommandLineOption deviceInfoOption_ = {QStringList{ "d", "device-scan"}, Main::tr("Print device-scan results.")}; + const QCommandLineOption logLvlOption_ = {QStringList{ "l", "log-level" }, Main::tr("Set log level (dbg,inf,wrn,err)."), "lvl"}; + const QCommandLineOption disableUInputOption_ = {QStringList{ "disable-uinput" }, Main::tr("Disable uinput support.")}; + const QCommandLineOption showDlgOnStartOption_ = {QStringList{ "show-dialog" }, Main::tr("Show preferences dialog on start.")}; + const QCommandLineOption dialogMinOnlyOption_ = {QStringList{ "m", "minimize-only" }, Main::tr("Only allow minimizing the dialog.")}; + const QCommandLineOption disableOverlayOption_ = {QStringList{ "disable-overlay" }, Main::tr("Disable spotlight overlay completely.")}; + const QCommandLineOption additionalDeviceOption_ = {QStringList{ "D", "additional-device"}, Main::tr("Additional accepted device; DEVICE = vendorId:productId\n" " " - "e.g., -D 04b3:310c; e.g. -D 0x0c45:0x8101"), "device"); + "e.g., -D 04b3:310c; e.g. -D 0x0c45:0x8101"), "device"}; - parser.addOptions({versionOption, helpOption, fullHelpOption, commandOption, - cfgFileOption, fullVersionOption, deviceInfoOption, logLvlOption, - disableUInputOption, showDlgOnStartOption, dialogMinOnlyOption, - disableOverlayOption, additionalDeviceOption}); + // --------------------------------------------------------------------------------------------- + ProjecteurCmdLineParser() + { + parser.addOptions({versionOption_, helpOption_, fullHelpOption_, commandOption_, + cfgFileOption_, fullVersionOption_, deviceInfoOption_, logLvlOption_, + disableUInputOption_, showDlgOnStartOption_, dialogMinOnlyOption_, + disableOverlayOption_, additionalDeviceOption_}); + } - const QStringList args = [argc, &argv]() + // --------------------------------------------------------------------------------------------- + bool versionOptionSet() const { return parser.isSet(versionOption_); } + bool fullVersionOptionSet() const { return parser.isSet(fullVersionOption_); } + bool helpOptionSet() const { return parser.isSet(helpOption_); } + bool fullHelpOptionSet() const { return parser.isSet(fullHelpOption_); } + bool additionalDeviceOptionSet() const { return parser.isSet(additionalDeviceOption_); } + auto additionalDeviceOptionValues() const { return parser.values(additionalDeviceOption_); } + bool deviceInfoOptionSet() const { return parser.isSet(deviceInfoOption_); } + bool commandOptionSet() const { return parser.isSet(commandOption_); } + bool disableUInputOptionSet() const { return parser.isSet(disableUInputOption_); } + bool showDlgOnStartOptionSet() const { return parser.isSet(showDlgOnStartOption_); } + bool dialogMinOnlyOptionSet() const { return parser.isSet(dialogMinOnlyOption_); } + bool disableOverlayOptionSet() const { return parser.isSet(disableOverlayOption_); } + auto commandOptionValues() const { return parser.values(commandOption_); } + bool cfgFileOptionSet() const { return parser.isSet(cfgFileOption_); } + auto cfgFileOptionValue() const { return parser.value(cfgFileOption_); } + bool logLvlOptionSet() const { return parser.isSet(logLvlOption_); } + auto logLvlOptionValue() const { return parser.value(logLvlOption_); } + + // --------------------------------------------------------------------------------------------- + void processArgs(int argc, char** argv) { - const QStringList qtAppKeyValueOptions = { - "-platform", "-platformpluginpath", "-platformtheme", "-plugin", "-display" - }; - const QStringList qtAppSingleOptions = {"-reverse"}; - QStringList args; - for (int i = 0; i < argc; ++i) - { // Skip some default arguments supported by QtGuiApplication, we don't want to parse them - // but they will get passed through to the ProjecteurApp. - if (qtAppKeyValueOptions.contains(argv[i])) { ++i; } - else if (qtAppSingleOptions.contains(argv[i])) { continue; } - else { args.push_back(argv[i]); } - } - return args; - }(); + const QStringList args = [argc, &argv]() + { + const QStringList qtAppKeyValueOptions = { + "-platform", "-platformpluginpath", "-platformtheme", "-plugin", "-display" + }; + const QStringList qtAppSingleOptions = {"-reverse"}; + QStringList args; + for (int i = 0; i < argc; ++i) + { // Skip some default arguments supported by QtGuiApplication, we don't want to parse them + // but they will get passed through to the ProjecteurApp. + if (qtAppKeyValueOptions.contains(argv[i])) { ++i; } + else if (qtAppSingleOptions.contains(argv[i])) { continue; } + else { args.push_back(argv[i]); } + } + return args; + }(); + + parser.process(args); + } + + // --------------------------------------------------------------------------------------------- + auto value(const QCommandLineOption& option) const { return parser.value(option); } + auto isSet(const QCommandLineOption& option) const { return parser.isSet(option); } + auto values(const QCommandLineOption& option) const { return parser.values(option); } - parser.process(args); - if (parser.isSet(helpOption) || parser.isSet(fullHelpOption)) + // --------------------------------------------------------------------------------------------- + void printHelp(bool fullHelp) { print() << QCoreApplication::applicationName() << " " << projecteur::version_string() << std::endl; print() << "Usage: projecteur [OPTION]..." << std::endl; print() << ""; - print() << " -h, --help " << helpOption.description(); - print() << " --help-all " << fullHelpOption.description(); - print() << " -v, --version " << versionOption.description(); - print() << " --cfg FILE " << cfgFileOption.description(); - print() << " -d, --device-scan " << deviceInfoOption.description(); - print() << " -l, --log-level LEVEL " << logLvlOption.description(); - print() << " -D DEVICE " << additionalDeviceOption.description(); - if (parser.isSet(fullHelpOption)) { - print() << " --disable-uinput " << disableUInputOption.description(); - print() << " --show-dialog " << showDlgOnStartOption.description(); - print() << " -m, --minimize-only " << dialogMinOnlyOption.description(); + print() << " -h, --help " << helpOption_.description(); + print() << " --help-all " << fullHelpOption_.description(); + print() << " -v, --version " << versionOption_.description(); + print() << " --cfg FILE " << cfgFileOption_.description(); + print() << " -d, --device-scan " << deviceInfoOption_.description(); + print() << " -l, --log-level LEVEL " << logLvlOption_.description(); + print() << " -D DEVICE " << additionalDeviceOption_.description(); + if (fullHelp) { + print() << " --disable-uinput " << disableUInputOption_.description(); + print() << " --show-dialog " << showDlgOnStartOption_.description(); + print() << " -m, --minimize-only " << dialogMinOnlyOption_.description(); } - print() << " -c COMMAND|PROPERTY " << commandOption.description() << std::endl; + print() << " -c COMMAND|PROPERTY " << commandOption_.description() << std::endl; print() << ""; print() << " spot=[on|off|toggle] " << Main::tr("Turn spotlight on/off or toggle."); print() << " settings=[show|hide] " << Main::tr("Show/hide preferences dialog."); - if (parser.isSet(fullHelpOption)) { + if (fullHelp) { print() << " preset=NAME " << Main::tr("Set a preset."); } print() << " quit " << Main::tr("Quit the running instance."); // Early return if the user not explicitly requested the full help - if (!parser.isSet(fullHelpOption)) return 0; + if (!fullHelp) { return; } print() << "\n" << ""; - // Helper function to get the range of valid values for a string property - const auto getValues = [](const Settings::StringProperty& sp) -> QString - { - if (sp.type == Settings::StringProperty::Type::Integer - || sp.type == Settings::StringProperty::Type::Double) { - return QString("(%1 ... %2)").arg(sp.range[0].toString(), sp.range[1].toString()); - } - else if (sp.type == Settings::StringProperty::Type::Bool) { - return "(false, true)"; - } - else if (sp.type == Settings::StringProperty::Type::Color) { - return "(HTML-color; #RRGGBB)"; - } - else if (sp.type == Settings::StringProperty::Type::StringEnum) { - QStringList values; - for (const auto& v : sp.range) { - values.push_back(v.toString()); - } - return QString("(%1)").arg(values.join(", ")); - } - return QString(); - }; - int maxPropertyStringLength = 0; const std::vector> propertiesList = - [getValues=std::move(getValues), &maxPropertyStringLength]() + [&maxPropertyStringLength]() { std::vector> list; // Fill temporary list with properties to be able to format our output better @@ -167,7 +308,7 @@ int main(int argc, char *argv[]) { list.emplace_back( QString("%1=[%2]").arg(sp.first, sp.second.typeToString(sp.second.type)), - getValues(sp.second)); + getValuesDescription(sp.second)); maxPropertyStringLength = qMax(maxPropertyStringLength, list.back().first.size()); } @@ -177,117 +318,51 @@ int main(int argc, char *argv[]) for (const auto& sp : propertiesList) { print() << " " << std::left << std::setw(maxPropertyStringLength + 3) << sp.first << sp.second; } + } + }; + +} // end anonymous namespace + + +// ------------------------------------------------------------------------------------------------- +int main(int argc, char *argv[]) +{ + QCoreApplication::setApplicationName("Projecteur"); + QCoreApplication::setApplicationVersion(projecteur::version_string()); + ProjecteurApplication::Options options; + QStringList ipcCommands; + { + ProjecteurCmdLineParser parser; + parser.processArgs(argc, argv); + if (parser.helpOptionSet() || parser.fullHelpOptionSet()) + { + parser.printHelp(parser.fullHelpOptionSet()); return 0; } - if (parser.isSet(additionalDeviceOption)) { - for (auto& deviceValue : parser.values(additionalDeviceOption)) { - const auto devAttribs = deviceValue.split(":"); - const auto vendorId = devAttribs[0].toUShort(nullptr, 16); - const auto productId = devAttribs[1].toUShort(nullptr, 16); - if (vendorId == 0 || productId == 0) { - error() << Main::tr("Invalid vendor/productId pair: ") << deviceValue; - } else { - const QString name = (devAttribs.size() >= 3) ? devAttribs[2] : ""; - options.additionalDevices.push_back({vendorId, productId, false, name}); - } - } + if (parser.additionalDeviceOptionSet()) { + addDevices(options, parser.additionalDeviceOptionValues()); } - if (parser.isSet(versionOption) || parser.isSet(fullVersionOption)) + // Print version information, if option is set + if (parser.versionOptionSet() || parser.fullVersionOptionSet()) { - print() << QCoreApplication::applicationName().toStdString() << " " - << projecteur::version_string(); - - if (parser.isSet(fullVersionOption) || - (std::string(projecteur::version_branch()) != "master" && - std::string(projecteur::version_branch()) != "not-within-git-repo")) - { // Not a build from master branch, print out additional information: - print() << " - git-branch: " << projecteur::version_branch(); - print() << " - git-hash: " << projecteur::version_fullhash(); - } - - // Show if we have a build from modified sources - if (projecteur::version_isdirty()) - print() << " - dirty-flag: " << projecteur::version_isdirty(); - - // Additional useful information - if (parser.isSet(fullVersionOption)) - { - print() << " - compiler: " << XSTRINGIFY(CXX_COMPILER_ID) << " " - << XSTRINGIFY(CXX_COMPILER_VERSION); - print() << " - build-type: " << projecteur::version_buildtype(); - print() << " - qt-version: (build: " << QT_VERSION_STR << ", runtime: " << qVersion() << ")"; - - const auto result = DeviceScan::getDevices(options.additionalDevices); - print() << " - device-scan: " - << QString("(errors: %1, devices: %2 [readable: %3, writable: %4])") - .arg(result.errorMessages.size()).arg(result.devices.size()) - .arg(result.numDevicesReadable).arg(result.numDevicesWritable); - } + printVersionInfo(options, parser.fullVersionOptionSet()); return 0; } - if (parser.isSet(deviceInfoOption)) + // Print device information if option is set + if (parser.deviceInfoOptionSet()) { - const auto result = DeviceScan::getDevices(options.additionalDevices); - print() << QCoreApplication::applicationName() << " " - << projecteur::version_string() << "; " << Main::tr("device scan") << std::endl; - - for (const auto& errmsg : result.errorMessages) { - print() << "** " << Main::tr("Error: ") << errmsg; - } - - print() << (result.errorMessages.size() ? "\n" : "") - << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") - .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); - - const auto busTypeToString = [](BusType type) -> QString { - if (type == BusType::Usb) return "USB"; - if (type == BusType::Bluetooth) return "Bluetooth"; - return "unknown"; - }; - - for (const auto& device : result.devices) - { - print() << "\n" - << " +++ " << "name: '" << device.name << "'"; - if (!device.userName.isEmpty()) { - print() << " " << "userName: '" << device.userName << "'"; - } - - const QStringList subDeviceList = [&device](){ - QStringList subDeviceList; - for (const auto& sd: device.subDevices) { - if (sd.deviceFile.size()) subDeviceList.push_back(sd.deviceFile); - } - return subDeviceList; - }(); - - const bool allReadable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), - [](const auto& subDevice){ - return subDevice.deviceReadable; - }); - - const bool allWriteable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), - [](const auto& subDevice){ - return subDevice.deviceWritable; - }); - - print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); - print() << " " << "productId: " << logging::hexId(device.id.productId); - print() << " " << "phys: " << device.id.phys; - print() << " " << "busType: " << busTypeToString(device.id.busType); - print() << " " << "devices: " << subDeviceList.join(", "); - print() << " " << "readable: " << (allReadable ? "true" : "false"); - print() << " " << "writable: " << (allWriteable ? "true" : "false"); - } + printDeviceInfo(options); return 0; } - else if (parser.isSet(commandOption)) + + // Check and trim ipc commands if set + if (parser.commandOptionSet()) { - ipcCommands = parser.values(commandOption); + ipcCommands = parser.commandOptionValues(); for (auto& value : ipcCommands) { value = value.trimmed(); } @@ -295,25 +370,25 @@ int main(int argc, char *argv[]) if (ipcCommands.isEmpty()) { error() << Main::tr("Command/Properties cannot be an empty string."); - return 44; + return PROJECTEUR_ERROR_EMPTY_COMMAND_PROPS; } } - if (parser.isSet(cfgFileOption)) { - options.configFile = parser.value(cfgFileOption); + if (parser.cfgFileOptionSet()) { + options.configFile = parser.cfgFileOptionValue(); } - options.enableUInput = !parser.isSet(disableUInputOption); - options.showPreferencesOnStart = parser.isSet(showDlgOnStartOption); - options.dialogMinimizeOnly = parser.isSet(dialogMinOnlyOption); - options.disableOverlay = parser.isSet(disableOverlayOption); + options.enableUInput = !parser.disableUInputOptionSet(); + options.showPreferencesOnStart = parser.showDlgOnStartOptionSet(); + options.dialogMinimizeOnly = parser.dialogMinOnlyOptionSet(); + options.disableOverlay = parser.disableOverlayOptionSet(); - if (parser.isSet(logLvlOption)) { - const auto lvl = logging::levelFromName(parser.value(logLvlOption)); + if (parser.logLvlOptionSet()) { + const auto lvl = logging::levelFromName(parser.logLvlOptionValue()); if (lvl != logging::level::unknown) { logging::setCurrentLevel(lvl); } else { - error() << Main::tr("Cannot set log level, unknown level: '%1'").arg(parser.value(logLvlOption)); + error() << Main::tr("Cannot set log level, unknown level: '%1'").arg(parser.logLvlOptionValue()); } } } @@ -321,19 +396,20 @@ int main(int argc, char *argv[]) RunGuard guard(QCoreApplication::applicationName()); if (!guard.tryToRun()) { - if (ipcCommands.size()) { + if (ipcCommands.size() > 0) { return ProjecteurCommandClientApp(ipcCommands, argc, argv).exec(); } error() << Main::tr("Another application instance is already running. Exiting."); - return 42; + return PROJECTEUR_ERROR_ANOTHER_INST_RUNNING; } - else if (ipcCommands.size()) + + if (ipcCommands.size() > 0) { // No other application instance running - but command option was used. logInfo(appMain) << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); logWarning(appMain) << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); error() << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); - return 43; + return PROJECTEUR_ERROR_NO_INSTANCE_FOUND; } ProjecteurApplication app(argc, argv, options); diff --git a/src/nativekeyseqedit.cc b/src/nativekeyseqedit.cc index 46c4ce48..c5a5dc06 100644 --- a/src/nativekeyseqedit.cc +++ b/src/nativekeyseqedit.cc @@ -19,7 +19,8 @@ namespace { // ----------------------------------------------------------------------------------------------- constexpr int maxKeyCount = 4; // Same as QKeySequence -} + constexpr int keySeqInterval = 950; +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- @@ -33,14 +34,12 @@ NativeKeySeqEdit::NativeKeySeqEdit(QWidget* parent) setAttribute(Qt::WA_MacShowFocusRect, true); m_timer->setSingleShot(true); - m_timer->setInterval(950); + m_timer->setInterval(keySeqInterval); connect(m_timer, &QTimer::timeout, this, [this](){ setRecording(false); }); } // ------------------------------------------------------------------------------------------------- -NativeKeySeqEdit::~NativeKeySeqEdit() -{ -} +NativeKeySeqEdit::~NativeKeySeqEdit() = default; // ------------------------------------------------------------------------------------------------- const NativeKeySequence& NativeKeySeqEdit::keySequence() const @@ -51,7 +50,7 @@ const NativeKeySequence& NativeKeySeqEdit::keySequence() const // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::setKeySequence(const NativeKeySequence& nks) { - if (nks == m_nativeSequence) return; + if (nks == m_nativeSequence) { return; } m_nativeSequence = nks; update(); @@ -61,7 +60,7 @@ void NativeKeySeqEdit::setKeySequence(const NativeKeySequence& nks) // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::clear() { - if (m_nativeSequence.count() == 0) return; + if (m_nativeSequence.count() == 0) { return; } m_nativeSequence.clear(); update(); @@ -106,7 +105,7 @@ QSize NativeKeySeqEdit::sizeHint() const } // ------------------------------------------------------------------------------------------------- -void NativeKeySeqEdit::paintEvent(QPaintEvent*) +void NativeKeySeqEdit::paintEvent(QPaintEvent* /* event */) { const QStyleOptionFrame option = styleOption(); @@ -153,7 +152,7 @@ void NativeKeySeqEdit::reset() // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::setRecording(bool doRecord) { - if (m_recording == doRecord) return; + if (m_recording == doRecord) { return; } m_recording = doRecord; @@ -268,11 +267,13 @@ void NativeKeySeqEdit::keyPressEvent(QKeyEvent* e) setRecording(true); return; } - else if (e->key() == Qt::Key_Delete) + + if (e->key() == Qt::Key_Delete) { clear(); return; } + QWidget::keyPressEvent(e); return; } @@ -318,11 +319,11 @@ void NativeKeySeqEdit::focusOutEvent(QFocusEvent* e) int NativeKeySeqEdit::getQtModifiers(Qt::KeyboardModifiers state) { int result = 0; - if (state & Qt::ControlModifier) result |= Qt::ControlModifier; - if (state & Qt::MetaModifier) result |= Qt::MetaModifier; - if (state & Qt::AltModifier) result |= Qt::AltModifier; - if (state & Qt::ShiftModifier) result |= Qt::ShiftModifier; - if (state & Qt::GroupSwitchModifier) result |= Qt::GroupSwitchModifier; + if (state & Qt::ControlModifier) { result |= Qt::ControlModifier; } + if (state & Qt::MetaModifier) { result |= Qt::MetaModifier; } + if (state & Qt::AltModifier) { result |= Qt::AltModifier; } + if (state & Qt::ShiftModifier) { result |= Qt::ShiftModifier; } + if (state & Qt::GroupSwitchModifier) { result |= Qt::GroupSwitchModifier; } return result; } @@ -368,10 +369,11 @@ int NativeKeySeqEdit::drawText(int startX, QPainter& p, const QStyleOption& opti option.rect.bottomRight()); p.save(); - if (option.state & QStyle::State_Selected) + if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::HighlightedText)); - else + } else { p.setPen(option.palette.color(QPalette::Text)); + } QRect br; p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); diff --git a/src/preferencesdlg.cc b/src/preferencesdlg.cc index 28e301dc..5c9358cf 100644 --- a/src/preferencesdlg.cc +++ b/src/preferencesdlg.cc @@ -11,9 +11,9 @@ #include "logging.h" #include "settings.h" +#include #include #include -#include #include #include #include @@ -53,7 +53,7 @@ namespace { { CURSOR_PATH "cursor-uparrow.png", {"Up Arrow Cursor", Qt::UpArrowCursor}}, { CURSOR_PATH "cursor-whatsthis.png", {"What't This Cursor", Qt::WhatsThisCursor}}, }; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, @@ -190,8 +190,9 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) deleteBtn->setEnabled(index > 0); m_presetCombo->setStyle(index == 0 ? &*m_presetComboStyle : normalComboStyle); - if (index > 0 && !m_presetCombo->currentText().isEmpty()) + if (index > 0 && !m_presetCombo->currentText().isEmpty()) { settings->loadPreset(m_presetCombo->currentText()); + } }); connect(newBtn, &QPushButton::clicked, this, [newBtn, settings, this]() @@ -232,7 +233,7 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) connect(deleteBtn, &QPushButton::clicked, this, [this, settings]() { - if (m_presetCombo->currentIndex() < 0) return; + if (m_presetCombo->currentIndex() < 0) { return; } settings->removePreset(m_presetCombo->currentText()); }); @@ -367,7 +368,7 @@ QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) // Function for updating all spotlight shape related widgets auto updateShapeSettingsWidgets = [settings, shapeCombo, shapeRotationSb, shapeRotationLabel, spotGrid, this]() { - if (shapeCombo->currentIndex() == -1) return; + if (shapeCombo->currentIndex() == -1) { return; } const QString shapeQml = shapeCombo->itemData(shapeCombo->currentIndex()).toString(); const auto& shapes = settings->spotShapes(); auto it = std::find_if(shapes.cbegin(), shapes.cend(), [&shapeQml](const Settings::SpotShape& s) { @@ -400,7 +401,7 @@ QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) int row = startRow; for (const auto& s : it->shapeSettings()) { - if (row >= startRow + maxRows) break; + if (row >= startRow + maxRows) { break; } spotGrid->addWidget(new QLabel(s.displayName(), this),row, 0); if (s.defaultValue().type() == QVariant::Int) { @@ -421,7 +422,7 @@ QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) connect(pm, &QQmlPropertyMap::valueChanged, spinbox, [s, spinbox, this](const QString& key, const QVariant& value) { - if (key != s.settingsKey() || !value.isValid()) return; + if (key != s.settingsKey() || !value.isValid()) { return; } spinbox->setValue(value.toInt()); resetPresetCombo(); }); @@ -725,7 +726,7 @@ QWidget* PreferencesDialog::createLogTabWidget() QString logFilter(tr("Log files (*.log *.txt)")); const auto logFile = QFileDialog::getSaveFileName(this, tr("Save log file"), defaultFile, logFilter, &logFilter); - if (logFile.isEmpty()) return; + if (logFile.isEmpty()) { return; } saveDir = QFileInfo(logFile).path(); QFile f(logFile); @@ -763,8 +764,9 @@ QWidget* PreferencesDialog::createLogTabWidget() // ------------------------------------------------------------------------------------------------- void PreferencesDialog::setMode(Mode dialogMode) { - if (m_dialogMode == dialogMode) + if (m_dialogMode == dialogMode) { return; + } setDialogMode(dialogMode); } @@ -794,14 +796,15 @@ void PreferencesDialog::setDialogMode(Mode dialogMode) // ------------------------------------------------------------------------------------------------- void PreferencesDialog::resetPresetCombo() { - if (m_presetCombo) m_presetCombo->setCurrentIndex(0); + if (m_presetCombo) { m_presetCombo->setCurrentIndex(0); } } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::setDialogActive(bool active) { - if (active == m_active) + if (active == m_active) { return; + } m_active = active; emit dialogActiveChanged(active); @@ -820,7 +823,7 @@ bool PreferencesDialog::event(QEvent* e) } // ------------------------------------------------------------------------------------------------- -void PreferencesDialog::closeEvent(QCloseEvent*) +void PreferencesDialog::closeEvent(QCloseEvent* /* ev */) { if (m_dialogMode == Mode::MinimizeOnlyDialog) { emit exitApplicationRequested(); diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index a13fcb72..632dcf50 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -35,7 +35,7 @@ namespace { QString localServerName() { return QCoreApplication::applicationName() + "_local_socket"; } -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Options& options) @@ -46,7 +46,7 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio , m_linuxDesktop(new LinuxDesktop(this)) , m_xcbOnWayland(QGuiApplication::platformName() == "xcb" && m_linuxDesktop->isWayland()) { - if (screens().size() < 1) + if (screens().empty()) { const auto title = tr("No Screens detected"); const auto text = tr("screens().size() returned a size < 1. Exiting."); @@ -65,10 +65,10 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio m_settings); m_settings->setOverlayDisabled(options.disableOverlay); - m_dialog.reset(new PreferencesDialog(m_settings, m_spotlight, - options.dialogMinimizeOnly - ? PreferencesDialog::Mode::MinimizeOnlyDialog - : PreferencesDialog::Mode::ClosableDialog)); + m_dialog = std::make_unique(m_settings, m_spotlight, + options.dialogMinimizeOnly + ? PreferencesDialog::Mode::MinimizeOnlyDialog + : PreferencesDialog::Mode::ClosableDialog); connect(&*m_dialog, &PreferencesDialog::testButtonClicked, this, [this](){ m_spotlight->setSpotActive(true); @@ -117,15 +117,16 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio connect(m_settings, &Settings::multiScreenOverlayEnabledChanged, this, [this](){ setupScreenOverlays(); }); connect(m_settings, &Settings::overlayDisabledChanged, this, [this](bool disabled){ if (disabled) { - if (m_spotlight->spotActive()) m_spotlight->setSpotActive(false); - else emit m_spotlight->spotActiveChanged(false); + if (m_spotlight->spotActive()) { m_spotlight->setSpotActive(false); } + else { emit m_spotlight->spotActiveChanged(false); } } else { QTimer::singleShot(0, this, [this](){ - if (m_spotlight->spotActive()) + if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); - else + } else { m_spotlight->setSpotActive(true); + } }); } }); @@ -134,83 +135,77 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio connect(this, &ProjecteurApplication::screenAdded, this, [this](){ setupScreenOverlays(); }); connect(this, &ProjecteurApplication::screenRemoved, this, [this](){ setupScreenOverlays(); }); - // add and connect 'Preferences' tray menu action - const auto actionPref = m_trayMenu->addAction(tr("&Preferences...")); - connect(actionPref, &QAction::triggered, this, [this](){ - this->showPreferences(true); - }); + // Setup the tray icon and menu + setupTrayIcon(); - // add and and connect 'About' tray menu action - const auto actionAbout = m_trayMenu->addAction(tr("&About")); - connect(actionAbout, &QAction::triggered, this, [this]() - { - if (!m_aboutDialog) { - m_aboutDialog = new AboutDialog(); - connect(m_aboutDialog, &QDialog::finished, this, [this](int){ - m_aboutDialog->deleteLater(); // No need to keep about dialog in memory, not that important - }); - } - - if (m_aboutDialog->isVisible()) { - m_aboutDialog->show(); - m_aboutDialog->raise(); - m_aboutDialog->activateWindow(); - } else { - m_aboutDialog->open(); - } - }); - - m_trayMenu->addSeparator(); - const auto actionQuit = m_trayMenu->addAction(tr("&Quit")); - connect(actionQuit, &QAction::triggered, this, [this](){ - m_qmlEngine->deleteLater(); // see: https://bugreports.qt.io/browse/QTBUG-81247 - this->quit(); + connect(this, &ProjecteurApplication::aboutToQuit, this, [this](){ + for (const auto window : m_overlayWindows) { window->close(); } + m_overlayWindows.clear(); }); - m_trayIcon->setContextMenu(&*m_trayMenu); - m_trayIcon->setIcon(QIcon(":/icons/projecteur-tray-64.png")); - m_trayIcon->show(); + // Setup the spotlight connections. + setupSpotlight(); - connect(&*m_trayIcon, &QSystemTrayIcon::activated, this, - [this](QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger) + // Open local server for local IPC commands, e.g. from other command line instances + QLocalServer::removeServer(localServerName()); + if (m_localServer->listen(localServerName())) + { + connect(m_localServer, &QLocalServer::newConnection, this, [this]() { - const auto trayGeometry = m_trayIcon->geometry(); - // This usually won't give us a valid geometry, since Qt isn't drawing the tray icon itself - if (trayGeometry.isValid()) { - m_trayIcon->contextMenu()->popup(m_trayIcon->geometry().center()); - } else { - // It's tricky to get the same behavior on all desktop environments. While on GNOME3 - // it behaves as one (or most) would expect, it behaves differently on other Desktop - // environments. - // QSystemTrayIcon is a wrapper around the StatusNotfierItem on modern (Linux) Desktops - // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ - // Via the Qt API there is not much control over how e.g. KDE or GNOME show the icon - // and how it behaves.. e.g. setting something like - // org.freedesktop.StatusNotifierItem.ItemIsMenu to True would be good for KDE Plasma - // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/ - this->showPreferences(true); - } - } - }); + while(QLocalSocket *clientConnection = m_localServer->nextPendingConnection()) + { + connect(clientConnection, &QLocalSocket::readyRead, this, [this, clientConnection]() { + this->readCommand(clientConnection); + }); + connect(clientConnection, &QLocalSocket::disconnected, this, [this, clientConnection]() { + const auto it = m_commandConnections.find(clientConnection); + if (it != m_commandConnections.end()) + { + quint32& commandSize = it->second; + while (clientConnection->bytesAvailable() && commandSize <= clientConnection->bytesAvailable()) { + this->readCommand(clientConnection); + } + m_commandConnections.erase(it); + } + clientConnection->close(); + clientConnection->deleteLater(); + }); - connect(&*m_dialog, &PreferencesDialog::exitApplicationRequested, actionQuit, [actionQuit]() { - logDebug(mainapp) << tr("Exit request from preferences dialog."); - actionQuit->trigger(); - }); + // Timeout timer - if after 5 seconds the connection is still open just disconnect... + const auto clientConnPtr = QPointer(clientConnection); + QTimer::singleShot(5000, clientConnection, [clientConnPtr](){ + if (clientConnPtr) { + // time out + clientConnPtr->disconnectFromServer(); + } + }); - connect(this, &ProjecteurApplication::aboutToQuit, this, [this](){ - for (const auto window : m_overlayWindows) { window->close(); } - m_overlayWindows.clear(); - }); + m_commandConnections.emplace(clientConnection, 0); + } + }); + } + else + { + logError(cmdserver) << tr("Error starting local socket for inter-process communication."); + } +} +// ------------------------------------------------------------------------------------------------- +ProjecteurApplication::~ProjecteurApplication() +{ + if (m_localServer) { m_localServer->close(); } +} + +// ------------------------------------------------------------------------------------------------- +void ProjecteurApplication::setupSpotlight() +{ // Handling of spotlight window when mouse move events from spotlight device are detected connect(m_spotlight, &Spotlight::spotActiveChanged, this, [this](bool active) { if (active && !m_settings->overlayDisabled()) { - if (!m_settings->multiScreenOverlayEnabled()) setScreenForCursorPos(); + if (!m_settings->multiScreenOverlayEnabled()) { setScreenForCursorPos(); } for (const auto window : m_overlayWindows) { @@ -248,7 +243,7 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio // Workaround for 'xcb' on Wayland session (default on Ubuntu) // .. the window in that case is not transparent for inputs and cannot be clicked through. // --> hide the window, although animations will not be visible - if (m_xcbOnWayland) window->hide(); + if (m_xcbOnWayland) { window->hide(); } } if (m_xcbOnWayland && m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog && m_dialog->isMinimized()) { // keep Window minimized... @@ -265,55 +260,75 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio m_dialog->activateWindow(); } }); +} - // Open local server for local IPC commands, e.g. from other command line instances - QLocalServer::removeServer(localServerName()); - if (m_localServer->listen(localServerName())) +// ------------------------------------------------------------------------------------------------- +void ProjecteurApplication::setupTrayIcon() +{ + // add and connect 'Preferences' tray menu action + const auto actionPref = m_trayMenu->addAction(tr("&Preferences...")); + connect(actionPref, &QAction::triggered, this, [this](){ + this->showPreferences(true); + }); + + // add and and connect 'About' tray menu action + const auto actionAbout = m_trayMenu->addAction(tr("&About")); + connect(actionAbout, &QAction::triggered, this, [this]() { - connect(m_localServer, &QLocalServer::newConnection, this, [this]() - { - while(QLocalSocket *clientConnection = m_localServer->nextPendingConnection()) - { - connect(clientConnection, &QLocalSocket::readyRead, this, [this, clientConnection]() { - this->readCommand(clientConnection); - }); - connect(clientConnection, &QLocalSocket::disconnected, this, [this, clientConnection]() { - const auto it = m_commandConnections.find(clientConnection); - if (it != m_commandConnections.end()) - { - quint32& commandSize = it->second; - while (clientConnection->bytesAvailable() && commandSize <= clientConnection->bytesAvailable()) { - this->readCommand(clientConnection); - } - m_commandConnections.erase(it); - } - clientConnection->close(); - clientConnection->deleteLater(); - }); + if (!m_aboutDialog) { + m_aboutDialog = new AboutDialog(); + connect(m_aboutDialog, &QDialog::finished, this, [this](int /* result */) { + m_aboutDialog->deleteLater(); // No need to keep about dialog in memory, not that important + }); + } - // Timeout timer - if after 5 seconds the connection is still open just disconnect... - const auto clientConnPtr = QPointer(clientConnection); - QTimer::singleShot(5000, clientConnection, [clientConnPtr](){ - if (clientConnPtr) { - // time out - clientConnPtr->disconnectFromServer(); - } - }); + if (m_aboutDialog->isVisible()) { + m_aboutDialog->show(); + m_aboutDialog->raise(); + m_aboutDialog->activateWindow(); + } else { + m_aboutDialog->open(); + } + }); - m_commandConnections.emplace(clientConnection, 0); + m_trayMenu->addSeparator(); + const auto actionQuit = m_trayMenu->addAction(tr("&Quit")); + connect(actionQuit, &QAction::triggered, this, [this](){ + m_qmlEngine->deleteLater(); // see: https://bugreports.qt.io/browse/QTBUG-81247 + this->quit(); + }); + m_trayIcon->setContextMenu(&*m_trayMenu); + + m_trayIcon->setIcon(QIcon(":/icons/projecteur-tray-64.png")); + m_trayIcon->show(); + + connect(&*m_trayIcon, &QSystemTrayIcon::activated, this, + [this](QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::Trigger) + { + const auto trayGeometry = m_trayIcon->geometry(); + // This usually won't give us a valid geometry, since Qt isn't drawing the tray icon itself + if (trayGeometry.isValid()) { + m_trayIcon->contextMenu()->popup(m_trayIcon->geometry().center()); + } else { + // It's tricky to get the same behavior on all desktop environments. While on GNOME3 + // it behaves as one (or most) would expect, it behaves differently on other Desktop + // environments. + // QSystemTrayIcon is a wrapper around the StatusNotfierItem on modern (Linux) Desktops + // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ + // Via the Qt API there is not much control over how e.g. KDE or GNOME show the icon + // and how it behaves.. e.g. setting something like + // org.freedesktop.StatusNotifierItem.ItemIsMenu to True would be good for KDE Plasma + // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/ + this->showPreferences(true); } - }); - } - else - { - logError(cmdserver) << tr("Error starting local socket for inter-process communication."); - } -} + } + }); -// ------------------------------------------------------------------------------------------------- -ProjecteurApplication::~ProjecteurApplication() -{ - if (m_localServer) m_localServer->close(); + connect(&*m_dialog, &PreferencesDialog::exitApplicationRequested, actionQuit, [actionQuit]() { + logDebug(mainapp) << tr("Exit request from preferences dialog."); + actionQuit->trigger(); + }); } // ------------------------------------------------------------------------------------------------- @@ -353,8 +368,9 @@ void ProjecteurApplication::cursorPositionChanged(const QPoint& pos) // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::updateOverlayWindow(QWindow* window, QScreen* screen) { - if (screen == nullptr) + if (screen == nullptr) { return; + } if (window->screen() == screen && screen->geometry() == window->geometry()) { return; @@ -388,10 +404,11 @@ void ProjecteurApplication::updateOverlayWindow(QWindow* window, QScreen* screen if (wasVisible && wasSpotActive) { QTimer::singleShot(0, this, [this](){ - if (m_spotlight->spotActive()) + if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); - else + } else { m_spotlight->setSpotActive(true); + } }); } } @@ -423,7 +440,7 @@ void ProjecteurApplication::setupScreenOverlays() m_screenWindowMap.clear(); const auto currentScreens = screens(); - if (currentScreens.size() == 0) + if (currentScreens.empty()) { for (const auto window : m_overlayWindows) { window->deleteLater(); } m_overlayWindows.clear(); @@ -439,7 +456,7 @@ void ProjecteurApplication::setupScreenOverlays() if (m_settings->multiScreenOverlayEnabled()) { const auto it = m_screenWindowMap.find(screen); - if (it == m_screenWindowMap.cend()) return; + if (it == m_screenWindowMap.cend()) { return; } updateOverlayWindow(it->second, it->first); } else { @@ -483,10 +500,11 @@ void ProjecteurApplication::setupScreenOverlays() // make sure it will be activated again. if (wasSpotActive) { QTimer::singleShot(0, this, [this](){ - if (m_spotlight->spotActive()) + if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); - else + } else { m_spotlight->setSpotActive(true); + } }); } } @@ -500,7 +518,7 @@ quint64 ProjecteurApplication::currentSpotScreen() const // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setCurrentSpotScreen(quint64 screen) { - if (m_currentSpotScreen == screen) return; + if (m_currentSpotScreen == screen) { return; } m_currentSpotScreen = screen; emit currentSpotScreenChanged(m_currentSpotScreen); } @@ -514,7 +532,7 @@ QPoint ProjecteurApplication::currentCursorPos() const // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setCurrentCursorPos(const QPoint& pos) { - if (pos == m_currentCursorPos) return; + if (pos == m_currentCursorPos) { return; } m_currentCursorPos = pos; emit currentCursorPosChanged(m_currentCursorPos); } @@ -531,8 +549,9 @@ void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) // Read size of command (always quint32) if not already done. if (commandSize == 0) { - if (clientConnection->bytesAvailable() < static_cast(sizeof(quint32))) + if (clientConnection->bytesAvailable() < static_cast(sizeof(quint32))) { return; + } QDataStream in(clientConnection); in >> commandSize; @@ -578,7 +597,7 @@ void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) else if (cmdKey == "preset") { logDebug(cmdserver) << tr("Received command preset = %1").arg(cmdValue); - if (!cmdValue.isEmpty()) m_settings->loadPreset(cmdValue); + if (!cmdValue.isEmpty()) { m_settings->loadPreset(cmdValue); } } else if (cmdValue.size()) { @@ -608,13 +627,14 @@ void ProjecteurApplication::showPreferences(bool show) m_dialog->show(); m_dialog->raise(); static const bool qtPlatformIsWayland = QGuiApplication::platformName().toLower().startsWith("wayland"); - if (!qtPlatformIsWayland) m_dialog->activateWindow(); + if (!qtPlatformIsWayland) { m_dialog->activateWindow(); } } else { - if (m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog) + if (m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog) { m_dialog->showMinimized(); - else + } else { m_dialog->hide(); + } } } @@ -647,7 +667,7 @@ ProjecteurCommandClientApp::ProjecteurCommandClientApp(const QStringList& ipcCom { for (const auto& ipcCommand : ipcCommands) { - if (ipcCommand.isEmpty()) continue; + if (ipcCommand.isEmpty()) { continue; } const QByteArray commandBlock = [&ipcCommand]() { diff --git a/src/projecteurapp.h b/src/projecteurapp.h index 05efd117..6a394c83 100644 --- a/src/projecteurapp.h +++ b/src/projecteurapp.h @@ -69,6 +69,9 @@ private slots: QPoint currentCursorPos() const; void setCurrentCursorPos(const QPoint& pos); + void setupTrayIcon(); + void setupSpotlight(); + private: std::unique_ptr m_trayIcon; std::unique_ptr m_trayMenu; diff --git a/src/settings.cc b/src/settings.cc index c18fa3bf..67283506 100644 --- a/src/settings.cc +++ b/src/settings.cc @@ -10,9 +10,9 @@ #include #include -#include #include #include +#include #include #include #include @@ -73,7 +73,7 @@ namespace { constexpr int inputSequenceInterval = 250; constexpr uint8_t vibrationLength = 0; constexpr uint8_t vibrationIntensity = 128; - } + } // end namespace defaultValue namespace ranges { constexpr Settings::SettingRange spotSize{ 5, 100 }; @@ -86,8 +86,8 @@ namespace { constexpr Settings::SettingRange zoomFactor{ 1.5, 20.0 }; constexpr Settings::SettingRange inputSequenceInterval{ 100, 950 }; - } - } + } // end namespace ranges + } // end namespace settings // ----------------------------------------------------------------------------------------------- bool toBool(const QString& value) { @@ -143,9 +143,7 @@ Settings::Settings(const QString& configFile, QObject* parent) } // ------------------------------------------------------------------------------------------------- -Settings::~Settings() -{ -} +Settings::~Settings() = default; // ------------------------------------------------------------------------------------------------- void Settings::init() @@ -198,8 +196,8 @@ void Settings::initializeStringProperties() for (const auto& shapeSetting : shape.shapeSettings()) { const auto pm = shapeSettings(shape.name()); - if (!pm || !pm->property(shapeSetting.settingsKey().toLocal8Bit()).isValid()) continue; - if (shapeSetting.defaultValue().type() != QVariant::Int) continue; + if (!pm || !pm->property(shapeSetting.settingsKey().toLocal8Bit()).isValid()) { continue; } + if (shapeSetting.defaultValue().type() != QVariant::Int) { continue; } const auto stringProperty = QString("spot.shape.%1.%2").arg(shape.name().toLower()) .arg(shapeSetting.settingsKey().toLower()); @@ -316,7 +314,7 @@ void Settings::shapeSettingsSetDefaults() { if (auto propertyMap = shapeSettings(shape.name())) { - const QString key = settingDefinition.settingsKey(); + const QString& key = settingDefinition.settingsKey(); if (propertyMap->property(key.toLocal8Bit()).isValid()) { propertyMap->setProperty(key.toLocal8Bit(), settingDefinition.defaultValue()); } else { @@ -339,7 +337,7 @@ void Settings::shapeSettingsLoad(const QString& preset) { if (auto propertyMap = shapeSettings(shape.name())) { - const QString key = settingDefinition.settingsKey(); + const QString& key = settingDefinition.settingsKey(); const QString settingsKey = section + QString("Shape.%1/%2").arg(shape.name()).arg(key); const QVariant loadedValue = m_settings->value(settingsKey, settingDefinition.defaultValue()); @@ -370,7 +368,7 @@ void Settings::shapeSettingsSavePreset(const QString& preset) { if (auto propertyMap = shapeSettings(shape.name())) { - const QString key = settingDefinition.settingsKey(); + const QString& key = settingDefinition.settingsKey(); const QString settingsKey = section + QString("Shape.%1/%2").arg(shape.name()).arg(key); m_settings->setValue(settingsKey, propertyMap->property(key.toLocal8Bit())); } @@ -506,8 +504,7 @@ void Settings::savePreset(const QString& preset) // ------------------------------------------------------------------------------------------------- void Settings::setShowSpotShade(bool show) { - if (show == m_showSpotShade) - return; + if (show == m_showSpotShade) { return; } m_showSpotShade = show; m_settings->setValue(::settings::showSpotShade, m_showSpotShade); @@ -518,8 +515,7 @@ void Settings::setShowSpotShade(bool show) // ------------------------------------------------------------------------------------------------- void Settings::setSpotSize(int size) { - if (size == m_spotSize) - return; + if (size == m_spotSize) { return; } m_spotSize = qMin(qMax(::settings::ranges::spotSize.min, size), ::settings::ranges::spotSize.max); m_settings->setValue(::settings::spotSize, m_spotSize); @@ -530,8 +526,7 @@ void Settings::setSpotSize(int size) // ------------------------------------------------------------------------------------------------- void Settings::setShowCenterDot(bool show) { - if (show == m_showCenterDot) - return; + if (show == m_showCenterDot) { return; } m_showCenterDot = show; m_settings->setValue(::settings::showCenterDot, m_showCenterDot); @@ -542,8 +537,7 @@ void Settings::setShowCenterDot(bool show) // ------------------------------------------------------------------------------------------------- void Settings::setDotSize(int size) { - if (size == m_dotSize) - return; + if (size == m_dotSize) { return; } m_dotSize = qMin(qMax(::settings::ranges::dotSize.min, size), ::settings::ranges::dotSize.max); m_settings->setValue(::settings::dotSize, m_dotSize); @@ -554,8 +548,7 @@ void Settings::setDotSize(int size) // ------------------------------------------------------------------------------------------------- void Settings::setDotColor(const QColor& color) { - if (color == m_dotColor) - return; + if (color == m_dotColor) { return; } m_dotColor = color; m_settings->setValue(::settings::dotColor, m_dotColor); @@ -578,8 +571,7 @@ void Settings::setDotOpacity(double opacity) // ------------------------------------------------------------------------------------------------- void Settings::setShadeColor(const QColor& color) { - if (color == m_shadeColor) - return; + if (color == m_shadeColor) { return; } m_shadeColor = color; m_settings->setValue(::settings::shadeColor, m_shadeColor); @@ -602,8 +594,7 @@ void Settings::setShadeOpacity(double opacity) // ------------------------------------------------------------------------------------------------- void Settings::setCursor(Qt::CursorShape cursor) { - if (cursor == m_cursor) - return; + if (cursor == m_cursor) { return; } m_cursor = qMin(qMax(static_cast(0), cursor), Qt::LastCursor); m_settings->setValue(::settings::cursor, static_cast(m_cursor)); @@ -614,8 +605,7 @@ void Settings::setCursor(Qt::CursorShape cursor) // ------------------------------------------------------------------------------------------------- void Settings::setSpotShape(const QString& spotShapeQmlComponent) { - if (m_spotShape == spotShapeQmlComponent) - return; + if (m_spotShape == spotShapeQmlComponent) { return; } const auto it = std::find_if(spotShapes().cbegin(), spotShapes().cend(), [&spotShapeQmlComponent](const SpotShape& s) { @@ -681,8 +671,7 @@ bool Settings::spotRotationAllowed() const // ------------------------------------------------------------------------------------------------- void Settings::setSpotRotationAllowed(bool allowed) { - if (allowed == m_spotRotationAllowed) - return; + if (allowed == m_spotRotationAllowed) { return; } m_spotRotationAllowed = allowed; emit spotRotationAllowedChanged(allowed); @@ -691,8 +680,7 @@ void Settings::setSpotRotationAllowed(bool allowed) // ------------------------------------------------------------------------------------------------- void Settings::setShowBorder(bool show) { - if (show == m_showBorder) - return; + if (show == m_showBorder) { return; } m_showBorder = show; m_settings->setValue(::settings::showBorder, m_showBorder); @@ -703,8 +691,7 @@ void Settings::setShowBorder(bool show) // ------------------------------------------------------------------------------------------------- void Settings::setBorderColor(const QColor& color) { - if (color == m_borderColor) - return; + if (color == m_borderColor) { return; } m_borderColor = color; m_settings->setValue(::settings::borderColor, m_borderColor); @@ -715,8 +702,7 @@ void Settings::setBorderColor(const QColor& color) // ------------------------------------------------------------------------------------------------- void Settings::setBorderSize(int size) { - if (size == m_borderSize) - return; + if (size == m_borderSize) { return; } m_borderSize = qMin(qMax(::settings::ranges::borderSize.min, size), ::settings::ranges::borderSize.max); m_settings->setValue(::settings::borderSize, m_borderSize); @@ -739,8 +725,7 @@ void Settings::setBorderOpacity(double opacity) // ------------------------------------------------------------------------------------------------- void Settings::setZoomEnabled(bool enabled) { - if (enabled == m_zoomEnabled) - return; + if (enabled == m_zoomEnabled) { return; } m_zoomEnabled = enabled; m_settings->setValue(::settings::zoomEnabled, m_zoomEnabled); @@ -763,7 +748,7 @@ void Settings::setZoomFactor(double factor) // ------------------------------------------------------------------------------------------------- void Settings::setMultiScreenOverlayEnabled(bool enabled) { - if (m_multiScreenOverlayEnabled == enabled) return; + if (m_multiScreenOverlayEnabled == enabled) { return; } m_multiScreenOverlayEnabled = enabled; m_settings->setValue(::settings::multiScreenOverlay, m_multiScreenOverlayEnabled); logDebug(lcSettings) << "multi-screen-overlay = " << m_multiScreenOverlayEnabled; @@ -773,7 +758,7 @@ void Settings::setMultiScreenOverlayEnabled(bool enabled) // ------------------------------------------------------------------------------------------------- void Settings::setOverlayDisabled(bool disabled) { - if (m_overlayDisabled == disabled) return; + if (m_overlayDisabled == disabled) { return; } m_overlayDisabled = disabled; emit overlayDisabledChanged(m_overlayDisabled); } @@ -841,9 +826,9 @@ InputMapConfig Settings::getDeviceInputMapConfig(const DeviceId& dId) { m_settings->setArrayIndex(i); const auto seq = m_settings->value("deviceSequence"); - if (!seq.canConvert()) continue; + if (!seq.canConvert()) { continue; } const auto conf = m_settings->value("mappedAction"); - if (!conf.canConvert()) continue; + if (!conf.canConvert()) { continue; } auto mappedAction = qvariant_cast(conf); if (mappedAction.action->type() == Action::Type::ScrollHorizontal) { mappedAction.action = GlobalActions::scrollHorizontal(); @@ -918,25 +903,27 @@ int PresetModel::rowCount(const QModelIndex& parent) const // ------------------------------------------------------------------------------------------------- QVariant PresetModel::data(const QModelIndex& index, int role) const { - if (index.row() > static_cast(m_presets.size())) + if (index.row() > static_cast(m_presets.size())) { return QVariant(); + } if (role == Qt::DisplayRole) { if (index.row() == 0) { return tr("Current Settings"); } - else { - return m_presets[index.row()-1]; - } + + return m_presets[index.row()-1]; } - else if (role == Qt::FontRole && index.row() == 0) + + if (role == Qt::FontRole && index.row() == 0) { QFont f; f.setItalic(true); return f; } - else if (role == Qt::ForegroundRole && index.row() == 0) { + + if (role == Qt::ForegroundRole && index.row() == 0) { return QColor(QGuiApplication::palette().color(QPalette::Disabled, QPalette::Text)); } @@ -947,7 +934,7 @@ QVariant PresetModel::data(const QModelIndex& index, int role) const void PresetModel::addPreset(const QString& preset) { const auto lb = std::lower_bound(m_presets.begin(), m_presets.end(), preset); - if (lb != m_presets.end() && *lb == preset) return; // Already exists + if (lb != m_presets.end() && *lb == preset) { return; } // Already exists const auto insertRow = std::distance(m_presets.begin(), lb) + 1; beginInsertRows(QModelIndex(), insertRow, insertRow); @@ -966,7 +953,7 @@ void PresetModel::removePreset(const QString& preset) { const auto r = std::equal_range(m_presets.begin(), m_presets.end(), preset); const auto count = std::distance(r.first, r.second); - if (count == 0) return; + if (count == 0) { return; } const auto startRow = std::distance(m_presets.begin(), r.first) + 1; diff --git a/src/spotlight.cc b/src/spotlight.cc index d2f37e83..cf4feb4a 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -3,8 +3,8 @@ #include "spotlight.h" -#include "deviceinput.h" #include "device-hidpp.h" +#include "deviceinput.h" #include "logging.h" #include "settings.h" #include "virtualdevice.h" @@ -29,7 +29,7 @@ namespace { // See details on workaround in onEventDataAvailable bool workaroundLogitechFirstMoveEvent = true; -} // --- end anonymous namespace +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- @@ -56,7 +56,7 @@ struct HoldButtonStatus m_nextPressed = nextPressed; m_backPressed = backPressed; - if (!nextPressed && !backPressed) m_moveKeyEvSeq.clear(); + if (!nextPressed && !backPressed) { m_moveKeyEvSeq.clear(); } } bool nextPressed() const { return m_nextPressed; } @@ -85,8 +85,9 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) , m_settings(settings) , m_holdButtonStatus(std::make_unique()) { + constexpr int spotlightActiveTimoutMs = 600; m_activeTimer->setSingleShot(true); - m_activeTimer->setInterval(600); + m_activeTimer->setInterval(spotlightActiveTimoutMs); connect(m_activeTimer, &QTimer::timeout, this, [this](){ setSpotActive(false); @@ -104,7 +105,8 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) // From detecting a change with inotify, the device needs some time to be ready for open, // otherwise opening the device will fail. // TODO: This interval seems to work, but it is arbitrary - there should be a better way. - m_connectionTimer->setInterval(800); + constexpr int delayedConnectionTimerIntervalMs = 800; + m_connectionTimer->setInterval(delayedConnectionTimerIntervalMs); connect(m_connectionTimer, &QTimer::timeout, this, [this]() { logDebug(device) << tr("New connection check triggered"); @@ -126,7 +128,7 @@ Spotlight::~Spotlight() = default; bool Spotlight::anySpotlightDeviceConnected() const { for (const auto& dc : m_deviceConnections) { - if (dc.second->subDeviceCount()) return true; + if (dc.second->subDeviceCount()) { return true; } } return false; } @@ -136,7 +138,7 @@ uint32_t Spotlight::connectedDeviceCount() const { uint32_t count = 0; for (const auto& dc : m_deviceConnections) { - if (dc.second->subDeviceCount()) ++count; + if (dc.second->subDeviceCount()) { ++count; } } return count; } @@ -144,9 +146,9 @@ uint32_t Spotlight::connectedDeviceCount() const // ------------------------------------------------------------------------------------------------- void Spotlight::setSpotActive(bool active) { - if (m_spotActive == active) return; + if (m_spotActive == active) { return; } m_spotActive = active; - if (!m_spotActive) m_activeTimer->stop(); + if (!m_spotActive) { m_activeTimer->stop(); } emit spotActiveChanged(m_spotActive); } @@ -189,14 +191,14 @@ int Spotlight::connectDevices() .arg(dc->deviceName(), hexId(dev.id.vendorId), hexId(dev.id.productId), scanSubDevice.deviceFile); continue; } - if (dc->hasSubDevice(scanSubDevice.deviceFile)) continue; + if (dc->hasSubDevice(scanSubDevice.deviceFile)) { continue; } std::shared_ptr subDeviceConnection = [&scanSubDevice, &dc, this]() -> std::shared_ptr { // Input event sub devices if (scanSubDevice.type == DeviceScan::SubDevice::Type::Event) { auto devCon = SubEventConnection::create(scanSubDevice, *dc); - if (addInputEventHandler(devCon)) return devCon; + if (addInputEventHandler(devCon)) { return devCon; } } // Hidraw sub devices else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { @@ -214,7 +216,7 @@ int Spotlight::connectDevices() // Remove device on socketReadError connect(&*hidppCon, &SubHidppConnection::socketReadError, this, [this, connPtr](){ - if (!connPtr) return; + if (!connPtr) { return; } const bool anyConnectedBefore = anySpotlightDeviceConnected(); connPtr->disconnect(); QTimer::singleShot(0, this, [this, devicePath=connPtr->path(), anyConnectedBefore](){ @@ -233,7 +235,7 @@ int Spotlight::connectDevices() QPointer connPtr(hidrawConn.get()); // Remove device on socketReadError connect(&*hidrawConn, &SubHidrawConnection::socketReadError, this, [this, connPtr](){ - if (!connPtr) return; + if (!connPtr) { return; } const bool anyConnectedBefore = anySpotlightDeviceConnected(); connPtr->disconnect(); QTimer::singleShot(0, this, [this, devicePath=connPtr->path(), anyConnectedBefore](){ @@ -250,7 +252,7 @@ int Spotlight::connectDevices() return std::shared_ptr(); }(); - if (!subDeviceConnection) continue; + if (!subDeviceConnection) { continue; } if (dc->subDeviceCount() == 0) { // Load Input mapping settings when first sub-device gets added. @@ -265,7 +267,7 @@ int Spotlight::connectDevices() static QString lastPreset; - connect(im, &InputMapper::actionMapped, this, [this](std::shared_ptr action) + connect(im, &InputMapper::actionMapped, this, [this](const std::shared_ptr& action) { if (action->type() == Action::Type::CyclePresets) { @@ -286,7 +288,7 @@ int Spotlight::connectDevices() } else if (action->type() == Action::Type::ScrollHorizontal || action->type() == Action::Type::ScrollVertical) { - if (!m_virtualDevice) return; + if (!m_virtualDevice) { return; } const int param = (action->type() == Action::Type::ScrollHorizontal) ? static_cast(action.get())->param @@ -301,13 +303,13 @@ int Spotlight::connectDevices() } else if (action->type() == Action::Type::VolumeControl) { - if (!m_virtualDevice) return; + if (!m_virtualDevice) { return; } auto param = static_cast(action.get())->param; uint16_t keyCode = (param > 0)? KEY_VOLUMEUP: KEY_VOLUMEDOWN; const std::vector curVolInputEvents = {{{}, EV_KEY, keyCode, 1}, {{}, EV_SYN, SYN_REPORT, 0}, {{}, EV_KEY, keyCode, 0}, {{}, EV_SYN, SYN_REPORT, 0},}; - if (param) m_virtualDevice->emitEvents(curVolInputEvents); + if (param) { m_virtualDevice->emitEvents(curVolInputEvents); } } }); @@ -324,7 +326,7 @@ int Spotlight::connectDevices() logInfo(device) << tr("Connected device: %1 (%2:%3)") .arg(devName, hexId(id.vendorId), hexId(id.productId)); emit deviceConnected(id, devName); - if (!anyConnectedBefore) emit anySpotlightDeviceConnectedChanged(true); + if (!anyConnectedBefore) { emit anySpotlightDeviceConnectedChanged(true); } }); } @@ -418,7 +420,7 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) { workaroundLogitechFirstMoveEvent = false; if(!logitechIsFirst) { - if (!spotActive()) setSpotActive(true); + if (!spotActive()) { setSpotActive(true); } } } else if (!m_activeTimer->isActive()) { @@ -426,7 +428,7 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) } m_activeTimer->start(); - if (m_virtualDevice) m_virtualDevice->emitEvents(buf.data(), buf.pos()); + if (m_virtualDevice) { m_virtualDevice->emitEvents(buf.data(), buf.pos()); } } else { // Forward events to input mapper for the device @@ -441,7 +443,7 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) buf.reset(); } - if (!isNonBlocking) break; + if (!isNonBlocking) { break; } } // end while loop } @@ -485,7 +487,7 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) // TODO This works quiet okay in combination with adjusting x and y values, // but needs to be a more solid option to accumulate the mass of move events // and consolidate them to a number of meaningful action special key events. - if (m_holdMoveEventTimer->isActive()) return; + if (m_holdMoveEventTimer->isActive()) { return; } m_holdMoveEventTimer->start(); // byte 4 : -1 for left movement, 0 for right movement @@ -502,7 +504,7 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) constexpr int divider = 5; constexpr int minimum = 5; constexpr int maximum = 10; - if (std::abs(param) < minimum) return 0; + if (std::abs(param) < minimum) { return 0; } const auto sign = (param == 0) ? 0 : ((param > 0) ? 1 : -1); return std::floor(1.0 * ((abs(param) > maximum)? sign * maximum : param) / divider); }; @@ -510,7 +512,7 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) const int adjustedX = getReducedParam(x); const int adjustedY = getReducedParam(y); - if (adjustedX == 0 && adjustedY == 0) return; + if (adjustedX == 0 && adjustedY == 0) { return; } static const auto scrollHAction = GlobalActions::scrollHorizontal(); scrollHAction->param = -adjustedX; @@ -523,7 +525,7 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) if (!connection->inputMapper()->recordingMode()) { - for (auto key_event : m_holdButtonStatus->moveKeyEventSeq()) { + for (const auto& key_event : m_holdButtonStatus->moveKeyEventSeq()) { connection->inputMapper()->addEvents(key_event); } } @@ -541,7 +543,7 @@ bool Spotlight::addInputEventHandler(std::shared_ptr connect QSocketNotifier* const readNotifier = connection->socketReadNotifier(); connect(readNotifier, &QSocketNotifier::activated, this, [this, connection=std::move(connection)](int fd) { - onEventDataAvailable(fd, *connection.get()); + onEventDataAvailable(fd, *connection); }); return true; diff --git a/src/spotshapes.cc b/src/spotshapes.cc index 60b067af..6cda9eb6 100644 --- a/src/spotshapes.cc +++ b/src/spotshapes.cc @@ -3,8 +3,8 @@ #include "spotshapes.h" -#include #include +#include #include @@ -14,7 +14,7 @@ namespace { SpotShapeNGon::qmlRegister(); return true; }(); -} +} // end anonymous namespace SpotShapeStar::SpotShapeStar(QQuickItem* parent) : QQuickItem (parent) { @@ -56,7 +56,7 @@ QSGNode* SpotShapeStar::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* u geometryNode->setGeometry(geometry); geometryNode->setFlag(QSGNode::OwnsGeometry, true); - QSGFlatColorMaterial* const material = new QSGFlatColorMaterial(); + auto* const material = new QSGFlatColorMaterial(); material->setColor(m_color); geometryNode->setMaterial(material); geometryNode->setFlag(QSGNode::OwnsMaterial); @@ -73,9 +73,9 @@ QSGNode* SpotShapeStar::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* u QSGGeometry::Point2D* const vertices = geometryNode->geometry()->vertexDataAsPoint2D(); const int numSegments = m_points * 2; - const float cx = static_cast(width()/2); // center X - const float cy = static_cast(height()/2); // center Y - const float deltaRad = static_cast((360.0 / m_points) * (M_PI/180.0)); + const auto cx = static_cast(width()/2); // center X + const auto cy = static_cast(height()/2); // center Y + const auto deltaRad = static_cast((360.0 / m_points) * (M_PI/180.0)); float theta = -static_cast(90.0 * M_PI/180.0); vertices[0].set(cx, cy); @@ -241,9 +241,9 @@ QSGNode* SpotShapeNGon::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* u } QSGGeometry::Point2D* const vertices = geometryNode->geometry()->vertexDataAsPoint2D(); - const float cx = static_cast(width()/2); // center X - const float cy = static_cast(height()/2); // center Y - const float deltaRad = static_cast((360.0 / m_sides) * (M_PI/180.0)); + const auto cx = static_cast(width()/2); // center X + const auto cy = static_cast(height()/2); // center Y + const auto deltaRad = static_cast((360.0 / m_sides) * (M_PI/180.0)); float theta = -static_cast(90.0 * M_PI/180.0); vertices[0].set(cx, cy); diff --git a/src/virtualdevice.cc b/src/virtualdevice.cc index 80809137..ecdd4570 100644 --- a/src/virtualdevice.cc +++ b/src/virtualdevice.cc @@ -15,11 +15,11 @@ LOGGING_CATEGORY(virtualdevice, "virtualdevice") namespace { class VirtualDevice_ : public QObject {}; // for i18n and logging -} +} // end anonymous namespace struct VirtualDevice::Token {}; -VirtualDevice::VirtualDevice(Token, int fd) +VirtualDevice::VirtualDevice(Token /* token */, int fd) : m_uinpFd(fd) {} @@ -97,7 +97,7 @@ std::shared_ptr VirtualDevice::create(const char* name, void VirtualDevice::emitEvents(const struct input_event input_events[], size_t num) { - if (!num) return; + if (!num) { return; } if (const ssize_t sz = sizeof(input_event) * num) { const auto bytesWritten = write(m_uinpFd, input_events, sz); From 471b4d31a763b0a3170d8a86187a4b4695ab6793 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 21 Sep 2021 21:33:32 +0200 Subject: [PATCH 075/110] fix: Set application description. --- src/main.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.cc b/src/main.cc index 58ac0593..fe2985e9 100644 --- a/src/main.cc +++ b/src/main.cc @@ -190,7 +190,7 @@ namespace { struct ProjecteurCmdLineParser { QCommandLineParser parser; - // parser.setApplicationDescription(Main::tr("Linux/X11 application for the Logitech Spotlight device.")); + const QCommandLineOption versionOption_ = {QStringList{ "v", "version"}, Main::tr("Print application version.")}; const QCommandLineOption fullVersionOption_ = QCommandLineOption{QStringList{ "f", "fullversion" }}; const QCommandLineOption helpOption_ = {QStringList{ "h", "help"}, Main::tr("Show command line usage.")}; @@ -211,6 +211,7 @@ namespace { // --------------------------------------------------------------------------------------------- ProjecteurCmdLineParser() { + parser.setApplicationDescription(Main::tr("Linux/X11 application for the Logitech Spotlight device.")); parser.addOptions({versionOption_, helpOption_, fullHelpOption_, commandOption_, cfgFileOption_, fullVersionOption_, deviceInfoOption_, logLvlOption_, disableUInputOption_, showDlgOnStartOption_, dialogMinOnlyOption_, From 659b865f6d7e10a117064fb204f783e6890e7cc5 Mon Sep 17 00:00:00 2001 From: Jahn Date: Wed, 22 Sep 2021 17:39:38 +0200 Subject: [PATCH 076/110] Add support to build against Qt6 --- .gitignore | 2 +- CMakeLists.txt | 77 +++++++++++++++++++++++++++++++---------- src/actiondelegate.cc | 28 +++++++-------- src/device-vibration.cc | 2 +- src/device.cc | 4 ++- src/deviceinput.cc | 15 ++++++-- src/devicescan.cc | 8 ++--- src/deviceswidget.cc | 2 +- src/inputseqedit.cc | 5 +++ src/linuxdesktop.cc | 16 ++++++--- src/nativekeyseqedit.cc | 4 +++ src/preferencesdlg.cc | 14 +++++--- src/preferencesdlg.h | 2 +- src/projecteurapp.cc | 3 ++ src/settings.cc | 16 +++++++++ 15 files changed, 146 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index c85722f0..0535854a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ CMakeLists.txt.user* .vscode .idea *.code-workspace -build +build-* build/* icons/icon-font/output/ diff --git a/CMakeLists.txt b/CMakeLists.txt index c2d5177e..f6a7c990 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,17 +26,47 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") include(GitVersion) include(Translation) -set(CMAKE_CXX_STANDARD 14) +set(QtVersionOptions "Auto" "5" "6") +set(PROJECTEUR_QT_VERSION "Auto" CACHE STRING "Choose the Qt version") +set_property(CACHE PROJECTEUR_QT_VERSION PROPERTY STRINGS ${QtVersionOptions}) + +list(FIND QtVersionOptions ${PROJECTEUR_QT_VERSION} index) +if(index EQUAL -1) + message(FATAL_ERROR "PROJECTEUR_QT_VERSION must be one of ${QtVersionOptions}") +endif() + +if ("${PROJECTEUR_QT_VERSION}" STREQUAL "Auto") + find_package(QT NAMES Qt6 Qt5 RCOMPONENTS Core REQUIRED) +else() + set(QT_VERSION_MAJOR ${PROJECTEUR_QT_VERSION}) +endif() + +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) +set(QT_PACKAGE_NAME Qt${QT_VERSION_MAJOR}) + +message(STATUS "Using Qt version: ${Qt${QT_VERSION_MAJOR}_VERSION}") + +if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0") + set(CMAKE_CXX_STANDARD 14) +else() + set(CMAKE_CXX_STANDARD 17) +endif() + set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) -find_package(Qt5 5.7 COMPONENTS Core Gui Quick Widgets REQUIRED) -find_package(Qt5 QUIET COMPONENTS X11Extras) -set(HAS_Qt5_X11Extras ${Qt5_FOUND}) -find_package(Qt5 QUIET COMPONENTS DBus) -set(HAS_Qt5_DBus ${Qt5_FOUND}) -find_package(Qt5 QUIET COMPONENTS QuickCompiler) -set(HAS_Qt5_QuickCompiler ${Qt5_FOUND}) + +find_package(${QT_PACKAGE_NAME} 5.7 REQUIRED COMPONENTS Core Gui Quick Widgets) + +if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0") + find_package(${QT_PACKAGE_NAME} QUIET COMPONENTS X11Extras) + set(HAS_Qt_X11Extras ${${QT_PACKAGE_NAME}_FOUND}) +else() + set(HAS_Qt_X11Extras 0) +endif() + +find_package(${QT_PACKAGE_NAME} QUIET COMPONENTS DBus) +set(HAS_Qt_DBus ${${QT_PACKAGE_NAME}_FOUND}) # Qt 5.8 seems to have issues with the way Projecteur shows the full screen overlay window, # let's warn the user about it. @@ -46,7 +76,7 @@ if(Qt5_VERSION VERSION_EQUAL "5.8" "please use a different Qt Version.") endif() -if (HAS_Qt5_QuickCompiler) +if (HAS_Qt_QuickCompiler) # Off by default, since this ties the application strictly to the Qt version # it is built with, see https://doc.qt.io/qt-5.12/qtquick-deployment.html#compiling-qml-ahead-of-time option(USE_QTQUICK_COMPILER "Use the QtQuickCompiler" OFF) @@ -59,10 +89,19 @@ if (HAS_Qt5_QuickCompiler) set_property(SOURCE "${resfile}" PROPERTY SKIP_AUTOMOC ON) endforeach() else() - qt5_add_resources(RESOURCES qml/qml.qrc) + if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0") + qt5_add_resources(RESOURCES qml/qml.qrc) + else() + qt6_add_resources(RESOURCES qml/qml.qrc) + endif() endif() endif() -qt5_add_resources(RESOURCES resources.qrc) + +if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0") + qt5_add_resources(RESOURCES resources.qrc) +else() + qt6_add_resources(RESOURCES resources.qrc) +endif() add_executable(projecteur src/main.cc src/enum-helper.h @@ -95,19 +134,21 @@ add_executable(projecteur target_include_directories(projecteur PRIVATE src) target_link_libraries(projecteur - PRIVATE Qt5::Core Qt5::Quick Qt5::Widgets + PRIVATE ${QT_PACKAGE_NAME}::Core ${QT_PACKAGE_NAME}::Quick ${QT_PACKAGE_NAME}::Widgets ) -if(HAS_Qt5_X11Extras) - target_link_libraries(projecteur PRIVATE Qt5::X11Extras) - target_compile_definitions(projecteur PRIVATE HAS_Qt5_X11Extras=1) +if(HAS_Qt_X11Extras) + if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0") + target_link_libraries(projecteur PRIVATE ${QT_PACKAGE_NAME}::X11Extras) + endif() + target_compile_definitions(projecteur PRIVATE HAS_Qt_X11Extras=1) else() message(STATUS "Compiling without Qt5::X11Extras.") endif() -if(HAS_Qt5_DBus) - target_link_libraries(projecteur PRIVATE Qt5::DBus) - target_compile_definitions(projecteur PRIVATE HAS_Qt5_DBus=1) +if(HAS_Qt_DBus) + target_link_libraries(projecteur PRIVATE ${QT_PACKAGE_NAME}::DBus) + target_compile_definitions(projecteur PRIVATE HAS_Qt_DBus=1) else() message(STATUS "Compiling without Qt5::DBus.") endif() diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc index 88ff1eb7..1a11491b 100644 --- a/src/actiondelegate.cc +++ b/src/actiondelegate.cc @@ -313,16 +313,16 @@ void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op const auto& item = imModel->configData(index); if (!item.action) { return; } - const auto symbol = [&item]() -> unsigned int { + const auto symbol = [&item]() -> QChar { switch(item.action->type()) { - case Action::Type::KeySequence: return Font::Icon::keyboard_4; - case Action::Type::CyclePresets: return Font::Icon::connection_8; - case Action::Type::ToggleSpotlight: return Font::Icon::power_on_off_11; - case Action::Type::ScrollHorizontal: return Font::Icon::cursor_21_rotated; - case Action::Type::ScrollVertical: return Font::Icon::cursor_21; - case Action::Type::VolumeControl: return Font::Icon::audio_6; + case Action::Type::KeySequence: return QChar(Font::Icon::keyboard_4); + case Action::Type::CyclePresets: return QChar(Font::Icon::connection_8); + case Action::Type::ToggleSpotlight: return QChar(Font::Icon::power_on_off_11); + case Action::Type::ScrollHorizontal: return QChar(Font::Icon::cursor_21_rotated); + case Action::Type::ScrollVertical: return QChar(Font::Icon::cursor_21); + case Action::Type::VolumeControl: return QChar(Font::Icon::audio_6); } - return 0; + return QChar(0); }(); if (symbol != 0) { @@ -359,12 +359,12 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* }; static std::vector items { - {Action::Type::KeySequence, Font::Icon::keyboard_4, tr("Key Sequence"), false}, - {Action::Type::CyclePresets, Font::Icon::connection_8, tr("Cycle Presets"), false}, - {Action::Type::ToggleSpotlight, Font::Icon::power_on_off_11, tr("Toggle Spotlight"), false}, - {Action::Type::ScrollHorizontal, Font::Icon::cursor_21_rotated, tr("Scroll Horizontal"), true}, - {Action::Type::ScrollVertical, Font::Icon::cursor_21, tr("Scroll Vertical"), true}, - {Action::Type::VolumeControl, Font::Icon::audio_6, tr("Volume Control"), true}, + {Action::Type::KeySequence, QChar(Font::Icon::keyboard_4), tr("Key Sequence"), false}, + {Action::Type::CyclePresets, QChar(Font::Icon::connection_8), tr("Cycle Presets"), false}, + {Action::Type::ToggleSpotlight, QChar(Font::Icon::power_on_off_11), tr("Toggle Spotlight"), false}, + {Action::Type::ScrollHorizontal, QChar(Font::Icon::cursor_21_rotated), tr("Scroll Horizontal"), true}, + {Action::Type::ScrollVertical, QChar(Font::Icon::cursor_21), tr("Scroll Vertical"), true}, + {Action::Type::VolumeControl, QChar(Font::Icon::audio_6), tr("Volume Control"), true}, }; static bool initIcons = []() diff --git a/src/device-vibration.cc b/src/device-vibration.cc index a315b025..18b29a94 100644 --- a/src/device-vibration.cc +++ b/src/device-vibration.cc @@ -49,7 +49,7 @@ struct TimerWidget::Impl const auto layout = new QHBoxLayout(parent); layout->addWidget(checkbox); layout->addWidget(stack); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); stack->addWidget(editor); stack->addWidget(overlay); diff --git a/src/device.cc b/src/device.cc index 74c14229..a8a4778d 100644 --- a/src/device.cc +++ b/src/device.cc @@ -21,7 +21,9 @@ LOGGING_CATEGORY(hid, "HID") namespace { // ----------------------------------------------------------------------------------------------- - static const auto registeredComparator_ = QMetaType::registerComparators(); + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + const auto registeredComparator_ = QMetaType::registerComparators(); + #endif const auto hexId = logging::hexId; // class i18n : public QObject {}; // for i18n and logging diff --git a/src/deviceinput.cc b/src/deviceinput.cc index b9c09c01..3f910e86 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -20,8 +20,11 @@ LOGGING_CATEGORY(input, "input") namespace { // ----------------------------------------------------------------------------------------------- + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) const auto registered_ = qRegisterMetaTypeStreamOperators() && qRegisterMetaTypeStreamOperators(); + #endif + // ----------------------------------------------------------------------------------------------- void addKeyToString(QString& str, const QString& key) @@ -31,7 +34,8 @@ namespace { } // ----------------------------------------------------------------------------------------------- - QKeySequence makeQKeySequence(const std::vector& keys) { + QKeySequence makeQKeySequence(const std::vector& keys) + { switch (keys.size()) { case 4: return QKeySequence(keys[0], keys[1], keys[2], keys[3]); case 3: return QKeySequence(keys[0], keys[1], keys[2]); @@ -365,7 +369,14 @@ QString NativeKeySequence::toString() const for (size_t i = 0; i < size; ++i) { if (i > 0) { seqString += QLatin1String(", "); } - seqString += toString(m_keySequence[i], + + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + const auto key = m_keySequence[i]; + #else + const auto key = m_keySequence[i].key(); + #endif + + seqString += toString(key, (i < m_nativeModifiers.size()) ? m_nativeModifiers[i] : to_integral(Modifier::NoModifier)); } diff --git a/src/devicescan.cc b/src/devicescan.cc index fa88e930..2cc2b4f4 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -127,11 +127,11 @@ namespace { const auto line = in.readLine(); for (const auto property : properties) { - if (line.startsWith(property) && line.size() > property->size() && line[property->size()] == '=') + if (line.startsWith(*property) && line.size() > property->size() && line[property->size()] == '=') { const QString value = line.mid(property->size() + 1); - if (property == hid_id) + if (*property == hid_id) { const auto ids = value.split(':'); const auto busType = ids.empty() ? 0: ids[0].toUShort(nullptr, 16); @@ -144,11 +144,11 @@ namespace { spotlightDevice.id.vendorId = ids.size() > 1 ? ids[1].toUShort(nullptr, 16) : 0; spotlightDevice.id.productId = ids.size() > 2 ? ids[2].toUShort(nullptr, 16) : 0; } - else if (property == hid_name) + else if (*property == hid_name) { spotlightDevice.name = value; } - else if (property == hid_phys) + else if (*property == hid_phys) { spotlightDevice.id.phys = value.split('/').first(); } diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 521e76ea..0573c8dc 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -141,7 +141,7 @@ QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createInputMapperWidget(Settings* settings, Spotlight* /*spotlight*/) { - const auto delShortcut = new QShortcut( QKeySequence(Qt::ShiftModifier + Qt::Key_Delete), this); + const auto delShortcut = new QShortcut( QKeySequence(Qt::ShiftModifier | Qt::Key_Delete), this); const auto imWidget = new QWidget(this); const auto layout = new QVBoxLayout(imWidget); diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 24dd2b6e..0af6fd86 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -189,8 +189,13 @@ QSize InputSeqEdit::sizeHint() const #endif const QStyleOptionFrame option = styleOption(); + + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) return (style()->sizeFromContents(QStyle::CT_LineEdit, &option, QSize(w, h). expandedTo(QApplication::globalStrut()), this)); + #else + return style()->sizeFromContents(QStyle::CT_LineEdit, &option, QSize(w, h), this); + #endif } // ------------------------------------------------------------------------------------------------- diff --git a/src/linuxdesktop.cc b/src/linuxdesktop.cc index 0e2efc44..72428b5c 100644 --- a/src/linuxdesktop.cc +++ b/src/linuxdesktop.cc @@ -6,13 +6,15 @@ #include "logging.h" #include -#include +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + #include +#endif #include #include #include #include -#if HAS_Qt5_DBus +#if HAS_Qt_DBus #include #include #endif @@ -20,7 +22,7 @@ LOGGING_CATEGORY(desktop, "desktop") namespace { -#if HAS_Qt5_DBus +#if HAS_Qt_DBus // ----------------------------------------------------------------------------------------------- QPixmap grabScreenDBusGnome() { @@ -55,7 +57,7 @@ namespace { } return pm; } -#endif // HAS_Qt5_DBus +#endif // HAS_Qt_DBus // ----------------------------------------------------------------------------------------------- QPixmap grabScreenVirtualDesktop(QScreen* screen) @@ -65,8 +67,12 @@ namespace { g = g.united(s->geometry()); } + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPixmap pm(QApplication::primaryScreen()->grabWindow( QApplication::desktop()->winId(), g.x(), g.y(), g.width(), g.height())); + #else + QPixmap pm(QApplication::primaryScreen()->grabWindow(0, g.x(), g.y(), g.width(), g.height())); + #endif if (!pm.isNull()) { @@ -129,7 +135,7 @@ QPixmap LinuxDesktop::grabScreen(QScreen* screen) const QPixmap LinuxDesktop::grabScreenWayland(QScreen* screen) const { -#if HAS_Qt5_DBus +#if HAS_Qt_DBus QPixmap pm; switch (type()) { diff --git a/src/nativekeyseqedit.cc b/src/nativekeyseqedit.cc index c5a5dc06..46d1e9e0 100644 --- a/src/nativekeyseqedit.cc +++ b/src/nativekeyseqedit.cc @@ -100,8 +100,12 @@ QSize NativeKeySeqEdit::sizeHint() const opt.fontMetrics.width(m_nativeSequence.toString())); #endif + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) return (style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(w, h). expandedTo(QApplication::globalStrut()), this)); + #else + return style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(w, h), this); + #endif } // ------------------------------------------------------------------------------------------------- diff --git a/src/preferencesdlg.cc b/src/preferencesdlg.cc index 5c9358cf..47020fce 100644 --- a/src/preferencesdlg.cc +++ b/src/preferencesdlg.cc @@ -31,8 +31,10 @@ #include #include -#if HAS_Qt5_X11Extras -#include +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + #if HAS_Qt_X11Extras + #include + #endif #endif #include @@ -147,7 +149,7 @@ QWidget* PreferencesDialog::createSettingsTabWidget(Settings* settings) const auto mainVBox = new QVBoxLayout(widget); mainVBox->addLayout(mainHBox); mainVBox->addWidget(presetSelector); -#if HAS_Qt5_X11Extras +#if HAS_Qt_X11Extras mainVBox->addWidget(createCompositorWarningWidget()); #endif mainVBox->addLayout(hbox); @@ -255,7 +257,7 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) } // ------------------------------------------------------------------------------------------------- -#if HAS_Qt5_X11Extras +#if HAS_Qt_X11Extras QWidget* PreferencesDialog::createCompositorWarningWidget() { if (!QX11Info::isPlatformX11()) @@ -403,7 +405,11 @@ QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) { if (row >= startRow + maxRows) { break; } spotGrid->addWidget(new QLabel(s.displayName(), this),row, 0); + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (s.defaultValue().type() == QVariant::Int) + #else + if (s.defaultValue().metaType().id() == QMetaType::Int) + #endif { const auto spinbox = new QSpinBox(this); spinbox->setMaximum(s.maxValue().toInt()); diff --git a/src/preferencesdlg.h b/src/preferencesdlg.h index e1159312..21a9b8b3 100644 --- a/src/preferencesdlg.h +++ b/src/preferencesdlg.h @@ -65,7 +65,7 @@ class PreferencesDialog : public QDialog QWidget* createMultiScreenWidget(Settings* settings); QGroupBox* createZoomGroupBox(Settings* settings); QWidget* createPresetSelector(Settings* settings); -#if HAS_Qt5_X11Extras +#if HAS_Qt_X11Extras QWidget* createCompositorWarningWidget(); #endif QWidget* createLogTabWidget(); diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index 632dcf50..210a6e24 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -11,7 +11,10 @@ #include "settings.h" #include "spotlight.h" +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #include +#endif + #include #include #include diff --git a/src/settings.cc b/src/settings.cc index 67283506..efa41dbe 100644 --- a/src/settings.cc +++ b/src/settings.cc @@ -197,7 +197,12 @@ void Settings::initializeStringProperties() { const auto pm = shapeSettings(shape.name()); if (!pm || !pm->property(shapeSetting.settingsKey().toLocal8Bit()).isValid()) { continue; } + + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (shapeSetting.defaultValue().type() != QVariant::Int) { continue; } + #else + if (shapeSetting.defaultValue().metaType().id() != QMetaType::Int) { continue; } + #endif const auto stringProperty = QString("spot.shape.%1.%2").arg(shape.name().toLower()) .arg(shapeSetting.settingsKey().toLower()); @@ -341,10 +346,17 @@ void Settings::shapeSettingsLoad(const QString& preset) const QString settingsKey = section + QString("Shape.%1/%2").arg(shape.name()).arg(key); const QVariant loadedValue = m_settings->value(settingsKey, settingDefinition.defaultValue()); + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (settingDefinition.defaultValue().type() == QVariant::Int // Currently only int shape settings supported && settingDefinition.defaultValue() != loadedValue) { logDebug(lcSettings) << QString("spot.shape.%1.%2 = ").arg(shape.name().toLower(), key) << loadedValue.toInt(); } + #else + if (settingDefinition.defaultValue().metaType().id() == QMetaType::Int // Currently only int shape settings supported + && settingDefinition.defaultValue() != loadedValue) { + logDebug(lcSettings) << QString("spot.shape.%1.%2 = ").arg(shape.name().toLower(), key) << loadedValue.toInt(); + } + #endif if (propertyMap->property(key.toLocal8Bit()).isValid()) { propertyMap->setProperty(key.toLocal8Bit(), loadedValue); @@ -394,7 +406,11 @@ void Settings::shapeSettingsInitialize() if (it != s.cend()) { + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (it->defaultValue().type() == QVariant::Int) // Currently only int shape settings supported + #else + if (it->defaultValue().metaType().id() == QMetaType::Int) + #endif { const auto setValue = value.toInt(); const auto min = it->minValue().toInt(); From 2fa3b02999c4c7efe3741f1be3d06270f808bc86 Mon Sep 17 00:00:00 2001 From: Jahn Date: Wed, 29 Sep 2021 22:07:16 +0200 Subject: [PATCH 077/110] Apply fixes for building against Qt6. --- CMakeLists.txt | 28 +++--- qml/main-qt6.qml | 213 +++++++++++++++++++++++++++++++++++++++++++ qml/main.qml | 4 +- qml/qml-qt6.qrc | 9 ++ src/deviceinput.cc | 2 +- src/deviceinput.h | 16 ++-- src/projecteurapp.cc | 5 + 7 files changed, 255 insertions(+), 22 deletions(-) create mode 100644 qml/main-qt6.qml create mode 100644 qml/qml-qt6.qrc diff --git a/CMakeLists.txt b/CMakeLists.txt index f6a7c990..c8349c40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,8 @@ endif() find_package(${QT_PACKAGE_NAME} QUIET COMPONENTS DBus) set(HAS_Qt_DBus ${${QT_PACKAGE_NAME}_FOUND}) +find_package(${QT_PACKAGE_NAME} QUIET COMPONENTS QuickCompiler) +set(HAS_Qt_QuickCompiler ${${QT_PACKAGE_NAME}_FOUND}) # Qt 5.8 seems to have issues with the way Projecteur shows the full screen overlay window, # let's warn the user about it. @@ -80,20 +82,22 @@ if (HAS_Qt_QuickCompiler) # Off by default, since this ties the application strictly to the Qt version # it is built with, see https://doc.qt.io/qt-5.12/qtquick-deployment.html#compiling-qml-ahead-of-time option(USE_QTQUICK_COMPILER "Use the QtQuickCompiler" OFF) +else() + set(USE_QTQUICK_COMPILER OFF) +endif() - if (USE_QTQUICK_COMPILER) - message(STATUS "Using QtQuick Compiler.") - qtquick_compiler_add_resources(RESOURCES qml/qml.qrc) - # Avoid CMake policy CMP0071 warning - foreach(resfile IN LISTS RESOURCES) - set_property(SOURCE "${resfile}" PROPERTY SKIP_AUTOMOC ON) - endforeach() +if (USE_QTQUICK_COMPILER) + message(STATUS "Using QtQuick Compiler.") + qtquick_compiler_add_resources(RESOURCES qml/qml.qrc) + # Avoid CMake policy CMP0071 warning + foreach(resfile IN LISTS RESOURCES) + set_property(SOURCE "${resfile}" PROPERTY SKIP_AUTOMOC ON) + endforeach() +else() + if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0") + qt5_add_resources(RESOURCES qml/qml.qrc) else() - if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0") - qt5_add_resources(RESOURCES qml/qml.qrc) - else() - qt6_add_resources(RESOURCES qml/qml.qrc) - endif() + qt6_add_resources(RESOURCES qml/qml-qt6.qrc) endif() endif() diff --git a/qml/main-qt6.qml b/qml/main-qt6.qml new file mode 100644 index 00000000..085d0d1f --- /dev/null +++ b/qml/main-qt6.qml @@ -0,0 +1,213 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md +import QtQuick 2.3 +import QtQuick.Window 2.2 + +import Qt5Compat.GraphicalEffects + +import Projecteur.Utils 1.0 as Utils + +Window { + id: mainWindow + property var screenId: -1 + readonly property bool spotOnCurrentWindow: ProjecteurApp.currentSpotScreen === screenId + property alias desktopPixmap: desktopImage.pixmap + + width: 300; height: 200 + + flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SplashScreen + + color: "transparent" + + readonly property double diagonal: Math.sqrt(Math.pow(Math.max(width, height),2)*2) + + Item { + id: rotationItem + anchors.centerIn: parent + width: rotation === 0 ? mainWindow.width : mainWindow.diagonal; + height: rotation === 0 ? mainWindow.height : width + rotation: Settings.spotRotationAllowed ? Settings.spotRotation : 0 + + opacity: ProjecteurApp.overlayVisible ? 1.0 : 0.0 + Behavior on opacity { PropertyAnimation { easing.type: Easing.OutQuad } } + + Item { + id: desktopItem + anchors.centerIn: centerRect + visible: false; enabled: false; clip: true + scale: Settings.zoomFactor + width: centerRect.width / scale; height: centerRect.height / scale + + Utils.Image { + id: desktopImage + smooth: rotation == 0 ? false : true + rotation: -rotationItem.rotation + readonly property real xOffset: Math.floor(parent.width/2.0 + ((rotationItem.width-mainWindow.width)/2)) + readonly property real yOffset: Math.floor(parent.height/2.0 + ((rotationItem.height-mainWindow.height)/2)) + x: -ma.mouseX + xOffset + y: -ma.mouseY + yOffset + width: mainWindow.width; height: mainWindow.height + } + } + + OpacityMask { + visible: Settings.zoomEnabled && mainWindow.spotOnCurrentWindow + cached: true + anchors.fill: centerRect + source: desktopItem + maskSource: spotShapeLoader.item + enabled: false + } + + Item { + anchors.fill: parent + MouseArea { + id: ma + + readonly property bool calculateMapping: Settings.multiScreenOverlayEnabled && !mainWindow.spotOnCurrentWindow + readonly property point globalPos: calculateMapping ? ProjecteurApp.currentCursorPos : Qt.point(0,0) + readonly property point mappedPos: calculateMapping ? mainWindow.contentItem.mapFromGlobal(globalPos.x, globalPos.y) : globalPos + readonly property int posX: spotOnCurrentWindow ? mouseX : mappedPos.x + readonly property int posY: spotOnCurrentWindow ? mouseY : mappedPos.y + + cursorShape: Settings.cursor + anchors.fill: parent + hoverEnabled: true + onClicked: { ProjecteurApp.spotlightWindowClicked() } + onExited: { ProjecteurApp.cursorExitedWindow() } + onEntered: { ProjecteurApp.cursorEntered(screenId) } + onPositionChanged: (mouse) => { + + if (Settings.multiScreenOverlayEnabled) { + ProjecteurApp.cursorPositionChanged( + mainWindow.contentItem.mapToGlobal(mouse.x, mouse.y)) + } + } + } + } + + Rectangle { + property int spotSize: (mainWindow.height / 100.0) * Settings.spotSize + id: centerRect + opacity: Settings.shadeOpacity + height: spotSize > 50 ? Math.min(spotSize, mainWindow.height) : 50 + width: height + x: ma.posX - width/2 + y: ma.posY - height/2 + color: Settings.shadeColor + visible: false + enabled: false + } + + Loader { + id: spotShapeLoader + visible: false; enabled: false + anchors.centerIn: centerRect + width: centerRect.width; height: width + sourceComponent: Qt.createComponent(Settings.spotShape) + } + + OpacityMask { + id: spot + visible: Settings.showSpotShade + opacity: centerRect.opacity + cached: true + invert: true + anchors.fill: centerRect + source: centerRect + maskSource: spotShapeLoader.item + enabled: false + } + + Loader { + id: borderShapeLoader + anchors.centerIn: centerRect + width: centerRect.width; height: width + visible: false; enabled: false + sourceComponent: spotShapeLoader.sourceComponent + onStatusChanged: { + if (status == Loader.Ready) { + borderShapeLoader.item.color = Qt.binding(function(){ return Settings.borderColor; }) + } + } + } + + Item { + id: borderShapeMask + anchors.centerIn: centerRect + width: centerRect.width; height: width + enabled: false; visible: false + Item { + id: borderShapeScaled + anchors.centerIn: parent + width: parent.width; height: width + scale: (100 - Settings.borderSize) * 1.0 / 100.0 + property Component component: borderShapeLoader.sourceComponent + property QtObject innerObject + onComponentChanged: { + if (innerObject) innerObject.destroy() + innerObject = component.createObject(borderShapeScaled, {visible: true}) + } + } + } + + OpacityMask { + id: spotBorder + visible: Settings.showBorder && Settings.borderSize > 0 + opacity: Settings.borderOpacity + cached: true + invert: true + anchors.fill: centerRect + source: borderShapeLoader.item + maskSource: borderShapeMask + enabled: false + } + + Rectangle { + id: dotCursor + antialiasing: true + anchors.centerIn: centerRect + width: Settings.dotSize; height: width + radius: width*0.5 + color: Settings.dotColor + visible: Settings.showCenterDot + opacity: Settings.dotOpacity + enabled: false + } + + Rectangle { + id: topRect + visible: spot.visible + color: centerRect.color + opacity: centerRect.opacity + anchors{ top: parent.top; bottom: centerRect.top; left: parent.left; right: parent.right } + enabled: false + } + + Rectangle { + id: bottomRect + visible: spot.visible + color: centerRect.color + opacity: centerRect.opacity + anchors{ top: centerRect.bottom; bottom: parent.bottom; left: parent.left; right: parent.right } + enabled: false + } + + Rectangle { + id: leftRect + visible: spot.visible + color: centerRect.color + opacity: centerRect.opacity + anchors{ top: topRect.bottom; bottom: bottomRect.top; left: parent.left; right: centerRect.left } + enabled: false + } + + Rectangle { + id: rightRect + visible: spot.visible + color: centerRect.color + opacity: centerRect.opacity + anchors{ top: topRect.bottom; bottom: bottomRect.top; left: centerRect.right; right: parent.right } + enabled: false + } + } +} // Window diff --git a/qml/main.qml b/qml/main.qml index 78a2e084..22a38c61 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -1,6 +1,7 @@ // This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md import QtQuick 2.3 import QtQuick.Window 2.2 + import QtGraphicalEffects 1.0 import Projecteur.Utils 1.0 as Utils @@ -74,7 +75,8 @@ Window { onClicked: { ProjecteurApp.spotlightWindowClicked() } onExited: { ProjecteurApp.cursorExitedWindow() } onEntered: { ProjecteurApp.cursorEntered(screenId) } - onPositionChanged: { + onPositionChanged: (mouse) => { + if (Settings.multiScreenOverlayEnabled) { ProjecteurApp.cursorPositionChanged( mainWindow.contentItem.mapToGlobal(mouse.x, mouse.y)) diff --git a/qml/qml-qt6.qrc b/qml/qml-qt6.qrc new file mode 100644 index 00000000..f90b4c54 --- /dev/null +++ b/qml/qml-qt6.qrc @@ -0,0 +1,9 @@ + + + main-qt6.qml + spotshapes/Circle.qml + spotshapes/Square.qml + spotshapes/Star.qml + spotshapes/Ngon.qml + + diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 3f910e86..789fdcae 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -886,4 +886,4 @@ const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key) return notFound; } -} // end namespace SpecialKeys \ No newline at end of file +} // end namespace SpecialKeys diff --git a/src/deviceinput.h b/src/deviceinput.h index b1bbf6ba..4560c360 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -40,14 +40,6 @@ struct DeviceInputEvent QDataStream& operator<<(QDataStream& s, const DeviceInputEvent& die); QDataStream& operator>>(QDataStream& s, DeviceInputEvent& die); -// ------------------------------------------------------------------------------------------------- -/// KeyEvent is a sequence of DeviceInputEvent. -using KeyEvent = std::vector; - -/// KeyEventSequence is a sequence of KeyEvents. -using KeyEventSequence = std::vector; -Q_DECLARE_METATYPE(KeyEventSequence); - // ------------------------------------------------------------------------------------------------- template QDataStream& operator<<(QDataStream& s, const std::vector& container) @@ -71,6 +63,14 @@ QDataStream& operator>>(QDataStream& s, std::vector& container) return s; } +// ------------------------------------------------------------------------------------------------- +/// KeyEvent is a sequence of DeviceInputEvent. +using KeyEvent = std::vector; + +/// KeyEventSequence is a sequence of KeyEvents. +using KeyEventSequence = std::vector; +Q_DECLARE_METATYPE(KeyEventSequence); + // ------------------------------------------------------------------------------------------------- QDebug operator<<(QDebug debug, const DeviceInputEvent &ie); QDebug operator<<(QDebug debug, const KeyEvent &ke); diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index 210a6e24..f1e016e3 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -107,7 +107,12 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio if (m_windowQmlComponent->status() != QQmlComponent::Status::Ready) { const auto title = tr("Overlay window error."); const auto text = tr("Qml component has status '%1'. Exiting.").arg(m_windowQmlComponent->status()); + logError(mainapp) << title << ";" << text; + for (const auto& error : m_windowQmlComponent->errors()) { + logError(mainapp) << error.toString(); + } + QMessageBox::critical(nullptr, title, text); QTimer::singleShot(0, this, [this](){ this->exit(2); }); return; From 69d7eb59fb9ab8ad7a5599ac5979ea2f663ec1e3 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Wed, 29 Sep 2021 22:17:56 +0200 Subject: [PATCH 078/110] Add apt update to codeql workflow --- .github/workflows/codeql-analysis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bf734bf8..531ff467 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -16,6 +16,7 @@ jobs: steps: - name: Install dependencies run: | + sudo apt-get update && \ sudo apt-get --no-install-recommends install pkg-config qtdeclarative5-dev \ qttools5-dev-tools qttools5-dev \ qt5-default libqt5x11extras5-dev From 704303869b189a5f61e397dbd38a04ffffd360f4 Mon Sep 17 00:00:00 2001 From: Jahn Date: Thu, 30 Sep 2021 18:30:00 +0200 Subject: [PATCH 079/110] Revert qml change to be compatible to older Qt versions. --- qml/main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qml/main.qml b/qml/main.qml index 22a38c61..bd970255 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -75,7 +75,7 @@ Window { onClicked: { ProjecteurApp.spotlightWindowClicked() } onExited: { ProjecteurApp.cursorExitedWindow() } onEntered: { ProjecteurApp.cursorEntered(screenId) } - onPositionChanged: (mouse) => { + onPositionChanged: { if (Settings.multiScreenOverlayEnabled) { ProjecteurApp.cursorPositionChanged( From 9db640a99b57e690e2c6d0da8c4affd0b69bdf90 Mon Sep 17 00:00:00 2001 From: Jahn Date: Thu, 30 Sep 2021 18:59:11 +0200 Subject: [PATCH 080/110] Update changelog for hotfix release v0.9.2 --- doc/CHANGELOG.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index db23d076..dffada31 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -6,8 +6,6 @@ - Logitech Spotlight Bluetooth vibration & hidraw support ([#140][p140]); - Logitech Spotlight Scrolling and Audio Volume functionality ([#85][i85]); -- Bug fix for high CPU load in certain situations ([#133][i133]) -- Bug fix for wrong button mapping for inputs with same length ([#144][i144]) - Added automated builds for Fedora 34, Debian 11 (Bullseye) and OpenSUSE 15.3 ([#148][i148]) - Bug fix for crash when closing the about dialog. @@ -16,11 +14,19 @@ support. [p140]: https://github.com/jahnf/Projecteur/pull/140 [i85]: https://github.com/jahnf/Projecteur/issues/85 -[i133]: https://github.com/jahnf/Projecteur/issues/133 -[i144]: https://github.com/jahnf/Projecteur/issues/144 [i148]: https://github.com/jahnf/Projecteur/issues/148 [c-mayanksuman]: https://github.com/mayanksuman +## v0.9.2 + +### Changes/Updates: + +- Bug fix for high CPU load in certain situations ([#133][i133]) +- Bug fix for wrong button mapping for inputs with same length ([#144][i144]) + +[i133]: https://github.com/jahnf/Projecteur/issues/133 +[i144]: https://github.com/jahnf/Projecteur/issues/144 + ## v0.9.1 ### Changes/Updates: From efa1c1b0d0369a47102ac473a1fa0680a812370a Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 3 Oct 2021 20:57:56 +0200 Subject: [PATCH 081/110] Add HID++ featureset cache. --- src/device-hidpp.cc | 2 +- src/hidpp.cc | 116 +++++++++++++++++++++++++++++++++++++++++--- src/hidpp.h | 20 ++++++-- 3 files changed, 127 insertions(+), 11 deletions(-) diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index 55e85574..361c4954 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -571,7 +571,7 @@ void SubHidppConnection::initPresenter(std::function cb) setPresenterState(PresenterState::Initializing); - m_featureSet.initFromDevice(makeSafeCallback( + m_featureSet.initFromDevice(deviceId(), makeSafeCallback( [this, cb=std::move(cb)](HIDPP::FeatureSet::State state) mutable { using FState = HIDPP::FeatureSet::State; diff --git a/src/hidpp.cc b/src/hidpp.cc index 04612e91..e23403f3 100644 --- a/src/hidpp.cc +++ b/src/hidpp.cc @@ -11,9 +11,25 @@ #include #include +#include +#include +#include +#include + DECLARE_LOGGING_CATEGORY(hid) namespace { + // ----------------------------------------------------------------------------------------------- + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + const auto registered_ = qRegisterMetaTypeStreamOperators() + && qRegisterMetaTypeStreamOperators(); + #endif + + // ----------------------------------------------------------------------------------------------- + constexpr char featureSetFilename[] = "DeviceFeatureSet.conf"; + constexpr char firmwareKey[] = "firmwareVersion"; + constexpr char featureTableKey[] = "featureTable"; + // ----------------------------------------------------------------------------------------------- namespace Defaults { constexpr uint8_t HidppSoftwareId = 7; @@ -59,6 +75,12 @@ namespace { std::uniform_int_distribution distribution; return distribution(gen); } + + // ----------------------------------------------------------------------------------------------- + QString settingsKey(const DeviceId& dId, const QString& key) { + return QString("Device_%1_%2/%3") + .arg(logging::hexId(dId.vendorId), logging::hexId(dId.productId), key); + } } // end anonymous namespace // ------------------------------------------------------------------------------------------------- @@ -467,9 +489,9 @@ void FeatureSet::getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t curre } // ------------------------------------------------------------------------------------------------- -void FeatureSet::initFromDevice(std::function cb) +void FeatureSet::initFromDevice(DeviceId dId, std::function cb) { - postSelf([this, cb=std::move(cb)]() mutable + postSelf([this, dId, cb=std::move(cb)]() mutable { if (m_connection == nullptr || m_state == State::Initialized || m_state == State::Initializing) { @@ -479,7 +501,8 @@ void FeatureSet::initFromDevice(std::function cb) setState(State::Initializing); - getMainFirmwareInfo(makeSafeCallback([this, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) mutable + getMainFirmwareInfo(makeSafeCallback( + [this, dId, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) mutable { logDebug(hid) << tr("getMainFirmwareInfo() => %1, fi.type = %2").arg(toString(res)) .arg(to_integral(fi.firmwareType())); @@ -488,11 +511,35 @@ void FeatureSet::initFromDevice(std::function cb) m_mainFirmwareInfo = std::move(fi); } - // Independent from firmware result try to get features - Q_UNUSED(res); + // --- Try to load feature set from cache file + const auto cacheFile = QStandardPaths::locate( + QStandardPaths::StandardLocation::AppLocalDataLocation, featureSetFilename); + + if (!cacheFile.isEmpty() && res == MsgResult::Ok && m_mainFirmwareInfo.isValid()) + { + // load feature set and return + QSettings settings(cacheFile, QSettings::NativeFormat); + const auto fw = settings.value(settingsKey(dId, firmwareKey)); + if (fw.canConvert()) + { + auto cacheFirmwareInfo = fw.value(); + if (cacheFirmwareInfo == m_mainFirmwareInfo) + { + const auto table = settings.value(settingsKey(dId, featureTableKey)); + if (table.canConvert()) + { + m_featureTable = table.value(); + logDebug(hid) << tr("Loaded feature set with %1 entries from local cache").arg(m_featureTable.size()); + setState(State::Initialized); + if (cb) { cb(m_state); } + return; + } + } + } + } getFeatureCount(makeSafeCallback( - [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) mutable + [this, dId, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) mutable { logDebug(hid) << tr("getFeatureCount() => %1, featureIndex = %2, count = %3") .arg(toString(res)).arg(featureIndex).arg(count); @@ -505,7 +552,7 @@ void FeatureSet::initFromDevice(std::function cb) } getFeatureIds(featureIndex, count, makeSafeCallback( - [this, cb=std::move(cb)](MsgResult res, FeatureTable&& ft) + [this, dId, cb=std::move(cb)](MsgResult res, FeatureTable&& ft) { if (res != MsgResult::Ok) { setState(State::Error); @@ -514,6 +561,18 @@ void FeatureSet::initFromDevice(std::function cb) { m_featureTable = std::move(ft); setState(State::Initialized); + + // Store feature table in cache file + const auto dataPath = QStandardPaths::writableLocation( + QStandardPaths::StandardLocation::AppLocalDataLocation); + + if (!dataPath.isEmpty() && m_mainFirmwareInfo.isValid()) + { + const auto cacheFile = QDir(dataPath).filePath(featureSetFilename); + QSettings settings(cacheFile, QSettings::NativeFormat); + settings.setValue(settingsKey(dId, firmwareKey), QVariant::fromValue(m_mainFirmwareInfo)); + settings.setValue(settingsKey(dId, featureTableKey), QVariant::fromValue(m_featureTable)); + } } if (cb) { cb(m_state); } @@ -713,3 +772,46 @@ const char* toString(HIDPP::Notification n) }; return "Notification::(unknown)"; } + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator<<(QDataStream& s, const HIDPP::FeatureSet::FeatureTable& ft) +{ + s << static_cast(ft.size()); + for (const auto& entry : ft) { + s << entry.first << entry.second; + } + return s; +} + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator>>(QDataStream& s, HIDPP::FeatureSet::FeatureTable& ft) +{ + quint64 size{}; + s >> size; + for (quint64 i = 0; i < size; ++i) { + HIDPP::FeatureSet::FeatureTable::key_type key; + HIDPP::FeatureSet::FeatureTable::mapped_type value; + s >> key; + s >> value; + ft.emplace(key, value); + } + return s; +} + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator<<(QDataStream& s, const HIDPP::FirmwareInfo& fi) +{ + const auto& msg = fi.msg(); + const auto data = QByteArray::fromRawData(reinterpret_cast(msg.data()), msg.dataSize()); + s << data; + return s; +} + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator>>(QDataStream& s, HIDPP::FirmwareInfo& fi) +{ + QByteArray data; + s >> data; + fi = HIDPP::FirmwareInfo(std::vector(data.begin(), data.end())); + return s; +} diff --git a/src/hidpp.h b/src/hidpp.h index 51d0ac62..9cd861ca 100644 --- a/src/hidpp.h +++ b/src/hidpp.h @@ -215,7 +215,8 @@ namespace HIDPP { bool isErrorResponseTo(const Message& other) const; auto data() { return m_data.data(); } - auto dataSize() { return m_data.size(); } + const auto data() const { return m_data.data(); } + auto dataSize() const { return m_data.size(); } auto& operator[](size_t i) { return m_data.operator[](i); } const auto& operator[](size_t i) const { return m_data.operator[](i); } QString hex() const; @@ -314,12 +315,14 @@ namespace HIDPP { FirmwareInfo(const FirmwareInfo&) = default; FirmwareInfo(FirmwareInfo&&) = default; FirmwareInfo& operator=(FirmwareInfo&&) = default; + bool operator==(const FirmwareInfo& other) const { return m_rawMsg == other.m_rawMsg; } FirmwareType firmwareType() const; QString firmwarePrefix() const; uint16_t firmwareVersion() const; uint16_t firmwareBuild() const; bool isValid() const { return firmwareType() != FirmwareType::Invalid; } + const HIDPP::Message& msg() const { return m_rawMsg; } private: HIDPP::Message m_rawMsg; @@ -333,11 +336,12 @@ namespace HIDPP { Q_OBJECT public: + using FeatureTable = std::map; enum class State : uint8_t { Uninitialized, Initializing, Initialized, Error }; FeatureSet(HidppConnectionInterface* connection, QObject* parent = nullptr); - void initFromDevice(std::function cb); + void initFromDevice(DeviceId dId, std::function cb); State state() const; uint8_t featureIndex(FeatureCode fc) const; @@ -349,7 +353,6 @@ namespace HIDPP { private: using MsgResult = HidppConnectionInterface::MsgResult; - using FeatureTable = std::map; void getFeatureIndex(FeatureCode fc, std::function cb); void getFeatureCount(std::function cb); @@ -372,9 +375,20 @@ namespace HIDPP { }; } //end namespace HIDPP +// ------------------------------------------------------------------------------------------------- const char* toString(HidppConnectionInterface::MsgResult r); const char* toString(HIDPP::Error e); const char* toString(HIDPP::FeatureSet::State s); const char* toString(HIDPP::FeatureCode fc); const char* toString(HIDPP::BatteryStatus bs); const char* toString(HIDPP::Notification n); + +// ------------------------------------------------------------------------------------------------- +Q_DECLARE_METATYPE(HIDPP::FeatureSet::FeatureTable); +QDataStream& operator<<(QDataStream& s, const HIDPP::FeatureSet::FeatureTable& ft); +QDataStream& operator>>(QDataStream& s, HIDPP::FeatureSet::FeatureTable& ft); + +// ------------------------------------------------------------------------------------------------- +Q_DECLARE_METATYPE(HIDPP::FirmwareInfo); +QDataStream& operator<<(QDataStream& s, const HIDPP::FirmwareInfo& fi); +QDataStream& operator>>(QDataStream& s, HIDPP::FirmwareInfo& fi); From cbb21fb3cbd41f8b92864983a34c5736cfd83b38 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 3 Oct 2021 22:34:52 +0200 Subject: [PATCH 082/110] Fix regression of text position in recording widget. --- src/inputseqedit.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 0af6fd86..a03c329e 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -209,11 +209,12 @@ void InputSeqEdit::paintEvent(QPaintEvent* /* paintEvent */) const bool recording = m_inputMapper && m_inputMapper->recordingMode(); const auto& fm = option.fontMetrics; - const int xPos = (option.rect.height()-fm.height()) / 2; + int xPos = (option.rect.height()-fm.height()) / 2; if (recording) { - drawRecordingSymbol(xPos, p, option); + const auto spacingX = QStaticText(" ").size().width(); + xPos += drawRecordingSymbol(xPos, p, option) + spacingX; if (m_recordedSequence.empty()) { drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")); } else { From 4b7ed74fe8bf5659126c0fd0290b0abba3333036 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sat, 18 Sep 2021 00:23:28 +0900 Subject: [PATCH 083/110] Fixed a link in hidpp doc --- doc/LogitechSpotlightHID++.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/LogitechSpotlightHID++.md b/doc/LogitechSpotlightHID++.md index 897190aa..4bd97613 100644 --- a/doc/LogitechSpotlightHID++.md +++ b/doc/LogitechSpotlightHID++.md @@ -193,7 +193,7 @@ methods of `SubHidppConnection` class in [device-hidpp.h](../src/device-hidpp.h) After reprogramming the Next and Back buttons, the spotlight device will send mouse movement data when either of these button are long-pressed and device is moved. The processing of these events are discussed in the -[following section](#response-to-`next-hold`-and-`back-hold`-keys). +[following section](#response-to-next-hold-and-back-hold-keys). For completeness, it should be noted that the official Logitech Spotlight software reprogram the click and double click events too by following HID++ From 5e48afc0e293a6d511ad4f8d9719b57566184185 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 3 Oct 2021 22:21:23 +0200 Subject: [PATCH 084/110] Update documentation. --- README.md | 52 ++++++------- doc/LogitechSpotlightHID++.md | 135 +++++++++++++++++----------------- 2 files changed, 95 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 4b57b35a..7853e3f8 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,22 @@ So here it is: a Linux application for the Logitech Spotlight. ## Table of Contents - * [Motivation](#motivation) - * [Features](#features) - * [Supported Environments](#supported-environments) - * [How it works](#how-it-works) - * [Download](#download) - * [Building](#building) - * [Installation/Running](#installationrunning) - * [Pre-requisites](#pre-requisites) - * [Application Menu](#application-menu) - * [Command Line Interface](#command-line-interface) - * [Scriptability / Keyboard shortcuts](#scriptability) - * [Using Projecteur without a device](#using-projecteur-without-a-device) - * [Device Support](#device-support) - * [Troubleshooting](#troubleshooting) - * [Changelog](#changelog) - * [License](#license) +* [Motivation](#motivation) +* [Features](#features) +* [Supported Environments](#supported-environments) +* [How it works](#how-it-works) +* [Download](#download) +* [Building](#building) +* [Installation/Running](#installationrunning) + * [Pre-requisites](#pre-requisites) + * [Application Menu](#application-menu) + * [Command Line Interface](#command-line-interface) + * [Scriptability / Keyboard shortcuts](#scriptability) + * [Using Projecteur without a device](#using-projecteur-without-a-device) + * [Device Support](#device-support) + * [Troubleshooting](#troubleshooting) +* [Changelog](#changelog) +* [License](#license) ## Features @@ -49,6 +49,7 @@ So here it is: a Linux application for the Logitech Spotlight. * Button mapping: * Map any button on the device to (almost) any keyboard combination. * Switch between (cycle through) custom spotlight presets. + * Audio Volume / Horizontal and Vertical Scrolling (Logitech Spotlight). * Vibration (Timer) Support for the Logitech Spotlight * Usable without a presenter device (e.g. for online presentations) @@ -100,10 +101,8 @@ The _Key Sequence_ action is particularly powerful as it can emit any user-defin keystroke. These keystrokes can invoke shortcut in presentation software (or any other software) being used. Similarly, the _Cycle Preset_ action can be used for cycling different spotlight presets. However, it should be noted that -presets might get reordered after program restart. If user want to maintain the -order of presets, please prepend the name of preset with number. For example, -in stead of naming `Pointer` and `Highlight`, name them `1.Pointer` and -`2.Highlight` to maintain the order. +presets are ordered alphabetically on program start. To retain a certain +order of your presets, you can prepend the preset name with a number. #### Hold Button Mapping for Logitech Spotlight @@ -111,12 +110,12 @@ Logitech Spotlight can send Hold event for Next and Back buttons as HID++ messages. Using this device feature, this program provides three different usage of the Next or Hold button. - 1. Button Tap - 2. Long-Press Event - 3. Button Hold followed by device movement or Hold Move Event +1. Button Tap +2. Long-Press Event +3. Button Hold and Move Event -In Input Mapper tab (Devices tab in Preferences dialog box), the first two -button usage (_i.e._ tap and long-press) can be mapped directly by tapping or +On the Input Mapper tab (Devices tab in Preferences dialog box), the first two +button usages (_i.e._ tap and long-press) can be mapped directly by tapping or long pressing the relevant button. For mapping the third button usage (_i.e._ Hold Move Event), please ensure that the device is active by pressing any button, and then right click in first column (Input Sequence) for any entry and select @@ -260,6 +259,9 @@ shortcuts in your window manager (e.g. GNOME) to run the commands `projecteur -c and `projecteur -c spot=off` or `projecteur -c spot=toggle`, and therefore turning the spot on and off with a keyboard shortcut. +A complete list the properties that can be set via the command line, can be +listed with the `--help-all` command line option. + ### Using Projecteur without a device You can use _Projecteur_ for your online presentations and video conferences without a presenter diff --git a/doc/LogitechSpotlightHID++.md b/doc/LogitechSpotlightHID++.md index 4bd97613..3159cee9 100644 --- a/doc/LogitechSpotlightHID++.md +++ b/doc/LogitechSpotlightHID++.md @@ -1,40 +1,42 @@ -# HID++ Basics +# Logitech Spotlight HID++ + +## HID++ Basics There are two major version of Logitech HID++ protocol. The Logitech spotlight supports HID++ protocol version 2.0+. This document provide information about HID++ version 2.0+ only. In the HID++ protocol two different types of messages -can be used for communication to/from Logitech Spotlight device. +can be used for communication to the Logitech Spotlight device. These two types of message are 1. Short Message: 7 bytes long. Default message scheme for USB Dongle. The - Spotlight device only supports short messages, when it is connected through - the USB dongle. + Spotlight device only supports short messages, when it is connected through + the USB dongle. - * First Byte: `0x10` + * First Byte: `0x10` - * Second Byte: Device code for which the message is meant (in case it - is sent from PC)/originated. `0xff` for USB dongle, `0x01` for - Logitech Spotlight device. + * Second Byte: Device code for which the message is meant (in case it + is sent from PC)/originated. `0xff` for USB dongle, `0x01` for + Logitech Spotlight device. - * Third Byte: Feature Index. Some of the featureIndex are `0x00` (for - Root feature: used for querying device details), - `0x80` (short set), `0x81` (short get). + * Third Byte: Feature Index. Some of the featureIndex are `0x00` (for + Root feature: used for querying device details), + `0x80` (short set), `0x81` (short get). - * Forth Byte: If third byte is not `0x80` or `0x81` then last 4 bits - (`forth_byte & 0xf0`) are function code and first 4 bits - (`forth_byte & 0x0f`) are software identification code. - Software identification code is random value in range of 0 to 15 - (used to differentiate traffic for different softwares). + * Forth Byte: If third byte is not `0x80` or `0x81` then last 4 bits + (`forth_byte & 0xf0`) are function code and first 4 bits + (`forth_byte & 0x0f`) are software identification code. + Software identification code is random value in range of 0 to 15 + (used to differentiate traffic for different softwares). - * Fifth - Seventh Bytes: Parameters/data + * Fifth - Seventh Bytes: Parameters/data 2. Long Message: 20 bytes long. Logitech Spotlight supports long messages in - any connection mode (through USB dongle and Bluetooth). In long messages, the - first byte is `0x11`, the next three bytes (second byte to forth byte) are the - same as in short messages. However, in long messages, there are additional - bytes (Fifth - Twentieth bytes) that can be used as parameters/data. + any connection mode (through USB dongle and Bluetooth). In long messages, the + first byte is `0x11`, the next three bytes (second byte to forth byte) are the + same as in short messages. However, in long messages, there are additional + bytes (Fifth - Twentieth bytes) that can be used as parameters/data. -Please note that the device response will have the first four bytes the same as +Please note that in device response the first four bytes will be the same as in the request message, if no error is reported. However, in case of an error, the third byte in the device response will be `0x8f` and first, second, forth and fifth byte in the device response will be same as the first, second, third @@ -54,7 +56,7 @@ device produced up until today. The feature code is part of the HID++ protocol and does not vary for different devices. Some of the well known HID++2 feature codes are: -| `Feature Code Name` | `Byte Value` | +| Feature Code Name | Byte Value | | -------------------------- | ------------:| | `ROOT` | `0x0000` | | `FEATURE_SET` | `0x0001` | @@ -115,30 +117,28 @@ The application can retrieve the entire FeatureSet table for the device with following steps: 1. Get the number of features supported by device: - - a. Get the _Feature Index_ corresponding to the FeatureSet code (`0x0001`). - - b. Get the number of features supported by sending the request message - `{0x10, 0x01, (FeatureSet Index), 0x0d, 0x00, 0x00, 0x00}` - (3rd byte is the Feature Index for FeatureSet Code; function code - is `0x00` and software identification code is `0x0d` in forth byte). - In the response, the 5th byte will be the number of features - supported, except the root feature. As stated above, Root feature - always has the Feature Index `0x00`. Hence, total number of features - supported is one plus the count obtained in the response. + * Get the _Feature Index_ corresponding to the FeatureSet code (`0x0001`). + * Get the number of features supported by sending the request message + `{0x10, 0x01, (FeatureSet Index), 0x0d, 0x00, 0x00, 0x00}` + (3rd byte is the Feature Index for FeatureSet Code; function code + is `0x00` and software identification code is `0x0d` in forth byte). + In the response, the 5th byte will be the number of features + supported, except the root feature. As stated above, Root feature + always has the Feature Index `0x00`. Hence, total number of features + supported is one plus the count obtained in the response. 2. Iterate over the Feature Indexes 1 to the number of features supported and - send the request (assuming Feature_Index for feature set is `0x01`; third byte) - `{0x10, 0x01, 0x01, 0x1d, Feature_Index, 0x00, 0x00}` (function code is `0x10` - and software identification code is `0x0d` in forth byte). The response will - contain the HID++ Feature Code at byte 5 and 6 as `uint16_t` and the Feature - Type at byte 7 for a valid Feature Index. In the Feature Type byte, if 7th bit - is set this means _Software Hidden_, if bit 8 is set this means - _Obsolete feature_. So, Software_Hidden = (`Feature_Type & (1<<6)`) and - Obsolete_Feature = (`Feature_Type & (1<<7)`). - Software Hidden or Obsolete features should not be handled by any application. - In case the Feature Index is not valid (i.e.,feature index > number of - feature supported) then `0x0200` will be in the response at byte 5 and 6. + send the request (assuming Feature_Index for feature set is `0x01`; third byte) + `{0x10, 0x01, 0x01, 0x1d, Feature_Index, 0x00, 0x00}` (function code is `0x10` + and software identification code is `0x0d` in forth byte). The response will + contain the HID++ Feature Code at byte 5 and 6 as `uint16_t` and the Feature + Type at byte 7 for a valid Feature Index. In the Feature Type byte, + if 7th bit is set this means _Software Hidden_, if bit 8 is set this means + _Obsolete feature_. So, Software_Hidden = (`Feature_Type & (1<<6)`) and + Obsolete_Feature = (`Feature_Type & (1<<7)`). + Software Hidden or Obsolete features should not be handled by any application. + In case the Feature Index is not valid (i.e.,feature index > number of + feature supported) then `0x0200` will be in the response at byte 5 and 6. The FeatureSet table for a device may change with a firmware update. The application should cache FeatureSet table along with Firmware version and only @@ -146,27 +146,28 @@ read FeatureSet table again if the firmware version has changed. This logic for getting FeatureSet table from device is implemented in `initFromDevice` method in `FeatureSet` class in [hidpp.h](../src/hidpp.h). -# Resetting Logitech Spotlight device +## Resetting Logitech Spotlight device Depending on the connection mode (USB dongle or Bluetooth), the Logitech Spotlight device can be reset with following HID++ message from the application: 1. Reset the USB dongle by sending following commands in sequence -``` - {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00} //get wireless notification and software connection status - {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00} // set sofware bit to false - {0x10, 0xff, 0x80, 0x02, 0x02, 0x00, 0x00} // initialize the USB dongle - {0x10, 0xff, 0x80, 0x00, 0x00, 0x09, 0x00} // set sofware bit to true -``` + + ```json + {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00} // get wireless notification and software connection status + {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00} // set sofware bit to false + {0x10, 0xff, 0x80, 0x02, 0x02, 0x00, 0x00} // initialize the USB dongle + {0x10, 0xff, 0x80, 0x00, 0x00, 0x09, 0x00} // set sofware bit to true + ``` 2. Load the FeatureSet table for the device (from pre-existing cache or from - the device if firmware version has changed by calling `initFromDevice` - method in `FeatureSet` class in [hidpp.h](../src/hidpp.h)). + the device if firmware version has changed by calling `initFromDevice` + method in `FeatureSet` class in [hidpp.h](../src/hidpp.h)). 3. Reset the Spotlight device with the Feature index for Reset Feature Code - from the FeatureSet table. If the Feature Index for Reset Feature Code is - `0x05`, then HID++ request message for resetting will be - `{0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}`. + from the FeatureSet table. If the Feature Index for Reset Feature Code is + `0x05`, then HID++ request message for resetting will be + `{0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}`. In addition to these steps, the Projecteur also pings the device by sending `{0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}` (function code `0x10` and software @@ -179,7 +180,7 @@ Further, Projecteur configures the Logitech device to send `Next Hold` and `Back Hold` events and resets the pointer speed to a default value with following HID++ commands: -``` +```json // enable next button hold (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xda - next button, 0x33 - hold event) {0x11, 0x01, 0x07, 0x3d, 0x00, 0xda, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // back button hold (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xdc - back button, 0x33 - hold event) @@ -199,7 +200,7 @@ For completeness, it should be noted that the official Logitech Spotlight software reprogram the click and double click events too by following HID++ commands: -``` +```json // Send click event as HID++ message (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xd8-click button, 0x33- hold event) {0x11, 0x01, 0x07, 0x3d, 0x00, 0xd8, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // Send double click as HID++ message (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xdf - double click) @@ -212,9 +213,9 @@ advantageous as it help in implementing Input Mapping feature that official Logitech Software lacks. However, this approach also makes porting Projecteur to different platforms more difficult. -# Important HID++ commands for Spotlight device +## Important HID++ commands for Spotlight device -## Wireless Notification on Activation/Deactivation of Spotlight Device +### Wireless Notification on Activation/Deactivation of Spotlight Device The Spotlight device sends a wireless notification if it gets activated. Wireless notification will be short HID++ message if the Spotlight device is @@ -229,13 +230,13 @@ A long HID++ wireless notification is only received for device activation. In this message, third byte will be the Feature Index for the Wireless Notification Feature Code (`0x1db4`). -## Vibration support +### Vibration support The spotlight device can vibrate if the HID++ message `{0x10, 0x01, (Feature Index for Presenter Control Feature Code), 0x1d, length, 0xe8, intensity}` is sent to it. In the message, length can range between `0x00` to `0x0a`. -## Battery Status +### Battery Status Battery status can be requested by sending request command `{0x10, 0x01, 0x06, 0x0d, 0x00, 0x00, 0x00}` (assuming the Feature Index for @@ -245,7 +246,7 @@ current battery level in percent, sixth byte shows the next reported battery level in percent (device do not report continuous battery level) and the seventh byte shows the state of battery with following possible values. -``` +```cpp enum class BatteryStatus : uint8_t {Discharging = 0x00, Charging = 0x01, AlmostFull = 0x02, @@ -257,7 +258,7 @@ enum class BatteryStatus : uint8_t {Discharging = 0x00, }; ``` -# Processing of device response +## Processing of device response All of the HID++ commands listed above result in response messages from the Spotlight device. For most messages, these responses from device are just the @@ -268,7 +269,7 @@ class in [device-hidpp.h](../src/device-hidpp.h). Description of HID++ messages from device to reprogrammed keys (`Next Hold` and `Back Hold`) are provided in following sub-section: -## Response to `Next Hold` and `Back Hold` keys +### Response to `Next Hold` and `Back Hold` keys The first HID++ message sent by Spotlight device at the start and end of any hold event is `{0x11, 0x01, 0x07, 0x00, (button_code), ...followed by zeroes ....}` @@ -289,7 +290,7 @@ The relevant functions for processing `Next Hold` and `Back Hold` are provided in `registerForNotifications` method in the `Spotlight` class ([spotlight.h](../src/spotlight.h)). -# Further information +## Further information For more information about HID++ protocol in general please check [logitech-hidpp module](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-logitech-hidpp.c) From 93cf4e716b913daa007ed946dba9e10532f308b6 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 5 Oct 2021 21:41:43 +0200 Subject: [PATCH 085/110] Set CPack debian package compression to xz. --- cmake/modules/LinuxPkgCPackConfig.cmake.in | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/modules/LinuxPkgCPackConfig.cmake.in b/cmake/modules/LinuxPkgCPackConfig.cmake.in index e38224f2..4f567623 100644 --- a/cmake/modules/LinuxPkgCPackConfig.cmake.in +++ b/cmake/modules/LinuxPkgCPackConfig.cmake.in @@ -43,6 +43,7 @@ set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION # Other settings set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "@PKG_HOMEPAGE@") set(CPACK_DEBIAN_PACKAGE_SECTION "@PKG_DEBIAN_SECTION@") +set(CPACK_DEBIAN_COMPRESSION_TYPE xz) # Set requires/depends set(CPACK_RPM_PACKAGE_REQUIRES "@PKG_DEPENDENCIES@") From 59e5527ae7dc3199a7c2f79832d5e57a90649e05 Mon Sep 17 00:00:00 2001 From: Mayank Suman Date: Sat, 18 Sep 2021 17:55:08 +0900 Subject: [PATCH 086/110] User-friendly name in Input Mapper The Input sequence is now drawn with meaningful text. The screenshot of input mapper button mapping is also updated in this commit. --- CMakeLists.txt | 1 + doc/screenshot-button-mapping.png | Bin 69589 -> 71055 bytes src/actiondelegate.cc | 5 +- src/common-input-seq.h | 18 ++++++ src/device-hidpp.cc | 2 +- src/deviceinput.cc | 16 +++++- src/deviceinput.h | 7 ++- src/inputmapconfig.cc | 5 +- src/inputseqedit.cc | 90 ++++++++++++++++++++++-------- src/spotlight.cc | 13 +++-- 10 files changed, 121 insertions(+), 36 deletions(-) create mode 100644 src/common-input-seq.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c8349c40..cb5c56ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,7 @@ add_executable(projecteur src/spotlight.cc src/spotlight.h src/spotshapes.cc src/spotshapes.h src/virtualdevice.h src/virtualdevice.cc + src/common-input-seq.h ${RESOURCES}) target_include_directories(projecteur PRIVATE src) diff --git a/doc/screenshot-button-mapping.png b/doc/screenshot-button-mapping.png index 3d30008763f50dffeab1ac4f8f61445a39c6161d..25d7eefefa4e165ee33142b64b70368db93a5382 100644 GIT binary patch literal 71055 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

    f7@IAHSna%-Fwf1kEJsl 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 + +#include + +namespace CommonKeyEventSeqInfo { + static const std::array commonInputSeq = {{ + {"Double Click", {{{EV_KEY, BTN_LEFT, 1}}, {{EV_KEY, BTN_LEFT, 0}}, {{EV_KEY, BTN_LEFT, 1}}, {{EV_KEY, BTN_LEFT, 0}}}}, + {"Click", {{{EV_KEY, BTN_LEFT, 1}}, {{EV_KEY, BTN_LEFT, 0}}}}, + {"Right Click", {{{EV_KEY, BTN_RIGHT, 1}}, {{EV_KEY, BTN_RIGHT, 0}}}}, + {"Next", {{{EV_KEY, KEY_RIGHT, 1}}, {{EV_KEY, KEY_RIGHT, 0}}}}, + {"Back", {{{EV_KEY, KEY_LEFT, 1}}, {{EV_KEY, KEY_LEFT, 0}}}}, + }}; +} diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index 361c4954..c783a248 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -706,8 +706,8 @@ void SubHidppConnection::updateDeviceFlags() specialInputs.clear(); featureFlagsSet |= DeviceFlags::NextHold; featureFlagsSet |= DeviceFlags::BackHold; - specialInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove)); specialInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove)); + specialInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove)); logDebug(hid) << tr("Subdevice '%1' reported %2 support.") .arg(path()).arg(toString(HIDPP::FeatureCode::ReprogramControlsV4)); } diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 789fdcae..570d4863 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -863,13 +863,23 @@ InputMapper::ReservedInputs& InputMapper::specialInputs() namespace SpecialKeys { // ------------------------------------------------------------------------------------------------- +// Functions that provide all special event sequences for a device. +// Currently, special event seqences are only defined for +// Logitech Spotlight device. Please note that all special key event +// sequences are not necessarily be move type Key Seqeuce. +// Move type Key Sequences for the device are stored in +// InputMapper::Impl::m_reservedInputs by SubHidppConnection::updateDeviceFlags. const std::map& keyEventSequenceMap() { static const std::map keyMap { - {Key::BackHoldMove, {InputMapper::tr("Back Hold Move"), - makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove))}}, + {Key::NextHold, {InputMapper::tr("Next Hold"), + makeSpecialKeyEventSequence(to_integral(Key::NextHold))}}, + {Key::BackHold, {InputMapper::tr("Back Hold"), + makeSpecialKeyEventSequence(to_integral(Key::BackHold))}}, {Key::NextHoldMove, {InputMapper::tr("Next Hold Move"), - makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove))}}, + makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove)), true}}, + {Key::BackHoldMove, {InputMapper::tr("Back Hold Move"), + makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove)), true}}, }; return keyMap; } diff --git a/src/deviceinput.h b/src/deviceinput.h index 4560c360..3feadb22 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -87,13 +87,16 @@ namespace SpecialKeys constexpr uint16_t userRange = 0x0e00; // 0x0e00 - 0x0eff enum class Key : uint16_t { - NextHoldMove = 0x0ff0, - BackHoldMove = 0x0ff1, + NextHold = 0x0e10, // must be in SpecialKeys user range + BackHold = 0x0e11, // must be in SpecialKeys user range + NextHoldMove = 0x0ff0, // must be in SpecialKeys range + BackHoldMove = 0x0ff1, // must be in SpecialKeys range }; struct SpecialKeyEventSeqInfo { QString name; KeyEventSequence keyEventSeq; + bool isMoveEvent = false; }; const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key); diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index 6aabe447..f860576a 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -168,7 +168,10 @@ void InputMapConfigModel::setInputSequence(const QModelIndex& index, const KeyEv const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); const bool isSpecialMoveInput = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), [&c](const auto& specialKeyInfo){ - return (c.deviceSequence == specialKeyInfo.second.keyEventSeq); + if (c.deviceSequence == specialKeyInfo.second.keyEventSeq) { + return specialKeyInfo.second.isMoveEvent; + } + return false; } ); diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index a03c329e..8e4c62de 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -6,6 +6,7 @@ #include "deviceinput.h" #include "inputmapconfig.h" #include "logging.h" +#include "common-input-seq.h" #include #include @@ -410,6 +411,20 @@ void InputSeqDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti const int xPos = (option.rect.height()-fm.height()) / 2; const auto& keySeq = imModel->configData(index).deviceSequence; const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); + const auto& commonKeysInfo = CommonKeyEventSeqInfo::commonInputSeq; + + // Separate out events of type EV_KEY only + const auto keySeqOnlyEV_KEY = [keySeq](){ + KeyEventSequence kes_comparision; + for (auto ke: keySeq) { + KeyEvent temp_ke; + for (auto k: ke){ + if (k.type == EV_KEY) temp_ke.emplace_back(k); + } + kes_comparision.emplace_back(temp_ke); + } + return kes_comparision; + }(); const auto it = std::find_if(specialKeysMap.cbegin(), specialKeysMap.cend(), [&keySeq](const auto& specialKeyInfo){ @@ -423,7 +438,36 @@ void InputSeqDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti } else { - drawKeyEventSequence(xPos, *painter, option, keySeq); + size_t i = 0, old_i = 0; + int xPosition = xPos; + while (i < keySeqOnlyEV_KEY.size()) + { + for (auto& kes: commonKeysInfo) { + if (kes.keyEventSeq.size() > keySeqOnlyEV_KEY.size() - i) continue; + auto KeqSeqPart = KeyEventSequence(keySeqOnlyEV_KEY.begin()+i, + keySeqOnlyEV_KEY.begin()+i+kes.keyEventSeq.size()); + if (KeqSeqPart == kes.keyEventSeq) { + i += KeqSeqPart.size(); + xPosition += drawPlaceHolderText(xPosition, *painter, option, kes.name, false); + break; + } + } + if (old_i == i) { + if (i+1 < keySeqOnlyEV_KEY.size() && isButtonTap(keySeqOnlyEV_KEY.at(i), keySeqOnlyEV_KEY.at(i+1))) { + xPosition += drawKeyEventSequence(xPosition, *painter, option, {keySeqOnlyEV_KEY.at(i), keySeqOnlyEV_KEY.at(i+1)}); + i += 2; + } + else + { + xPosition += drawKeyEventSequence(xPosition, *painter, option, {keySeqOnlyEV_KEY.at(i)}); + i++; + } + } + if (i < keySeqOnlyEV_KEY.size()) { + xPosition += drawPlaceHolderText(xPosition, *painter, option, ", ", false); + } + old_i = i; + } } if (option.state & QStyle::State_HasFocus) { @@ -532,30 +576,32 @@ void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* auto* const menu = new QMenu(parent); for (const auto& button : specialInputs) { - const auto qaction = menu->addAction(button.name); - connect(qaction, &QAction::triggered, this, [model, index, button](){ - model->setInputSequence(index, button.keyEventSeq); - const auto& currentItem = model->configData(index); - if (!currentItem.action) { - model->setItemActionType(index, Action::Type::ScrollHorizontal); - } - else - { - switch (currentItem.action->type()) + if (button.isMoveEvent) { + const auto qaction = menu->addAction(button.name); + connect(qaction, &QAction::triggered, this, [model, index, button](){ + model->setInputSequence(index, button.keyEventSeq); + const auto& currentItem = model->configData(index); + if (!currentItem.action) { + model->setItemActionType(index, Action::Type::ScrollVertical); + } + else { - case Action::Type::ScrollHorizontal: // [[fallthrough]]; - case Action::Type::ScrollVertical: // [[fallthrough]]; - case Action::Type::VolumeControl: { - // scrolling and volume control allowed for special input - break; - } - default: { - model->setItemActionType(index, Action::Type::ScrollVertical); - break; + switch (currentItem.action->type()) + { + case Action::Type::ScrollHorizontal: // [[fallthrough]]; + case Action::Type::ScrollVertical: // [[fallthrough]]; + case Action::Type::VolumeControl: { + // scrolling and volume control allowed for special input + break; + } + default: { + model->setItemActionType(index, Action::Type::ScrollVertical); + break; + } } } - } - }); + }); + } } menu->exec(globalPos); diff --git a/src/spotlight.cc b/src/spotlight.cc index cf4feb4a..6589fae9 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -36,10 +36,7 @@ namespace { // Hold button state. Very much Logitech Spotlight specific. struct HoldButtonStatus { - enum class Button : uint16_t { - Next = 0x0e10, // must be in SpecialKeys user range - Back = 0x0e11, // must be in SpecialKeys user range - }; + void setButtonsPressed(bool nextPressed, bool backPressed) { @@ -470,11 +467,15 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; if (!m_holdButtonStatus->nextPressed() && isNextPressed) { - connection->inputMapper()->addEvents(KeyEvent{{EV_KEY, to_integral(HoldButtonStatus::Button::Next), 1}}); + for (auto ke: SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold).keyEventSeq) { + connection->inputMapper()->addEvents(ke); + } } if (!m_holdButtonStatus->backPressed() && isBackPressed) { - connection->inputMapper()->addEvents(KeyEvent{{EV_KEY, to_integral(HoldButtonStatus::Button::Back), 1}}); + for (auto ke: SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold).keyEventSeq) { + connection->inputMapper()->addEvents(ke); + } } m_holdButtonStatus->setButtonsPressed(isNextPressed, isBackPressed); From 1bdf34678bfccaf69a5324a96135bb0a99cbf598 Mon Sep 17 00:00:00 2001 From: Jahn Date: Wed, 6 Oct 2021 21:02:56 +0200 Subject: [PATCH 087/110] Add device specific key lookup for user friendly name. --- CMakeLists.txt | 2 +- doc/screenshot-button-mapping.png | Bin 71055 -> 68263 bytes src/actiondelegate.cc | 13 +-- src/common-input-seq.h | 18 ---- src/device-defs.h | 4 +- src/device-hidpp.cc | 8 +- src/device-key-lookup.cc | 78 ++++++++++++++++ src/device-key-lookup.h | 13 +++ src/deviceinput.cc | 53 +++++++---- src/deviceinput.h | 20 ++--- src/deviceswidget.cc | 6 +- src/inputmapconfig.cc | 30 +++---- src/inputmapconfig.h | 9 +- src/inputseqedit.cc | 142 ++++++++++-------------------- src/inputseqedit.h | 9 +- src/spotlight.cc | 16 ++-- 16 files changed, 233 insertions(+), 188 deletions(-) delete mode 100644 src/common-input-seq.h create mode 100644 src/device-key-lookup.cc create mode 100644 src/device-key-lookup.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cb5c56ea..b7a317cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,7 @@ add_executable(projecteur src/colorselector.cc src/colorselector.h src/device.cc src/device.h src/device-hidpp.cc src/device-hidpp.h + src/device-key-lookup.cc src/device-key-lookup.h src/device-vibration.cc src/device-vibration.h src/deviceinput.cc src/deviceinput.h src/devicescan.cc src/devicescan.h @@ -133,7 +134,6 @@ add_executable(projecteur src/spotlight.cc src/spotlight.h src/spotshapes.cc src/spotshapes.h src/virtualdevice.h src/virtualdevice.cc - src/common-input-seq.h ${RESOURCES}) target_include_directories(projecteur PRIVATE src) diff --git a/doc/screenshot-button-mapping.png b/doc/screenshot-button-mapping.png index 25d7eefefa4e165ee33142b64b70368db93a5382..63d600a22f74115dcf3be20eb0eeceef5d332884 100644 GIT binary patch literal 68263 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 literal 71055 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 if (!item.action) { return; } - const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); - const bool isSpecialMoveInput = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), - [&item](const auto& specialKeyInfo){ - if (item.deviceSequence == specialKeyInfo.second.keyEventSeq) { - return specialKeyInfo.second.isMoveEvent; - } - return false; - } - ); - struct actionEntry { Action::Type type; QChar symbol; @@ -392,6 +382,9 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* auto* const menu = new QMenu(parent); + // Check if input sequence is a back or next hold move event. + const bool isSpecialMoveInput = !SpecialKeys::logitechSpotlightHoldMove(item.deviceSequence).name.isEmpty(); + for (const auto& entry : items) { if ((isSpecialMoveInput && entry.isMoveAction) || (!isSpecialMoveInput && !entry.isMoveAction)) { diff --git a/src/common-input-seq.h b/src/common-input-seq.h deleted file mode 100644 index 2e063773..00000000 --- a/src/common-input-seq.h +++ /dev/null @@ -1,18 +0,0 @@ -// This file is part of Projecteur - https://github.com/jahnf/projecteur -// - See LICENSE.md and README.md -#pragma once - -#include "deviceinput.h" -#include - -#include - -namespace CommonKeyEventSeqInfo { - static const std::array commonInputSeq = {{ - {"Double Click", {{{EV_KEY, BTN_LEFT, 1}}, {{EV_KEY, BTN_LEFT, 0}}, {{EV_KEY, BTN_LEFT, 1}}, {{EV_KEY, BTN_LEFT, 0}}}}, - {"Click", {{{EV_KEY, BTN_LEFT, 1}}, {{EV_KEY, BTN_LEFT, 0}}}}, - {"Right Click", {{{EV_KEY, BTN_RIGHT, 1}}, {{EV_KEY, BTN_RIGHT, 0}}}}, - {"Next", {{{EV_KEY, KEY_RIGHT, 1}}, {{EV_KEY, KEY_RIGHT, 0}}}}, - {"Back", {{{EV_KEY, KEY_LEFT, 1}}, {{EV_KEY, KEY_LEFT, 0}}}}, - }}; -} diff --git a/src/device-defs.h b/src/device-defs.h index 7b071d57..6539586d 100644 --- a/src/device-defs.h +++ b/src/device-defs.h @@ -26,8 +26,8 @@ struct DeviceId uint16_t vendorId = 0; uint16_t productId = 0; BusType busType = BusType::Unknown; - QString phys; // should be sufficient to differentiate between two devices of the same type - // - not tested, don't have two devices of any type currently. + QString phys{}; // should be sufficient to differentiate between two devices of the same type + // - not tested, don't have two devices of any type currently. inline bool operator==(const DeviceId& rhs) const { return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys); diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc index c783a248..a6e93682 100644 --- a/src/device-hidpp.cc +++ b/src/device-hidpp.cc @@ -701,13 +701,12 @@ void SubHidppConnection::updateDeviceFlags() featureFlagsUnset |= DeviceFlag::ReportBattery; } + InputMapper::SpecialMoveInputs specialMoveInputs; if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::ReprogramControlsV4)) { - auto& specialInputs = m_inputMapper->specialInputs(); - specialInputs.clear(); featureFlagsSet |= DeviceFlags::NextHold; featureFlagsSet |= DeviceFlags::BackHold; - specialInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove)); - specialInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove)); + specialMoveInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove)); + specialMoveInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove)); logDebug(hid) << tr("Subdevice '%1' reported %2 support.") .arg(path()).arg(toString(HIDPP::FeatureCode::ReprogramControlsV4)); } @@ -715,6 +714,7 @@ void SubHidppConnection::updateDeviceFlags() featureFlagsUnset |= DeviceFlags::NextHold; featureFlagsUnset |= DeviceFlags::BackHold; } + m_inputMapper->setSpecialMoveInputs(std::move(specialMoveInputs)); if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::PointerSpeed)) { featureFlagsSet |= DeviceFlags::PointerSpeed; diff --git a/src/device-key-lookup.cc b/src/device-key-lookup.cc new file mode 100644 index 00000000..68eff2c8 --- /dev/null +++ b/src/device-key-lookup.cc @@ -0,0 +1,78 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + +#include "device-key-lookup.h" + +#include "enum-helper.h" + +#include + +#include + +namespace { +// ------------------------------------------------------------------------------------------------- +inline uint32_t eHash(uint16_t type, uint16_t code) +{ + return ( (static_cast(type) << 16) + | (static_cast(code)) ); +} + +// ------------------------------------------------------------------------------------------------- +inline uint32_t eHash(const DeviceInputEvent& die) { + return eHash(die.type, die.code); +} + +// ------------------------------------------------------------------------------------------------- +uint32_t dHash(const DeviceId& dId) +{ + return (static_cast(dId.vendorId) << 16) | dId.productId; +} + +} // end anonymous namespace + +namespace KeyName +{ +// ------------------------------------------------------------------------------------------------- +const QString& lookup(const DeviceId& dId, const DeviceInputEvent& die) +{ + using KeyNameMap = std::unordered_map; + + static const KeyNameMap logitechSpotlightMapping = { + { eHash(EV_KEY, BTN_LEFT), QObject::tr("Click") }, + { eHash(EV_KEY, KEY_RIGHT), QObject::tr("Next") }, + { eHash(EV_KEY, KEY_LEFT), QObject::tr("Back") }, + { eHash(EV_KEY, to_integral(SpecialKeys::Key::NextHold)), + SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold).name }, + { eHash(EV_KEY, to_integral(SpecialKeys::Key::BackHold)), + SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold).name }, + }; + + static const KeyNameMap avattoH100Mapping = { + { eHash(EV_KEY, BTN_LEFT), QObject::tr("Click") }, + { eHash(EV_KEY, KEY_PAGEDOWN), QObject::tr("Down") }, + { eHash(EV_KEY, KEY_PAGEUP), QObject::tr("Up") }, + }; + + static const std::unordered_map map = + { + {dHash({0x046d, 0xc53e}), logitechSpotlightMapping}, // Spotlight USB + {dHash({0x046d, 0xb503}), logitechSpotlightMapping}, // Spotlight Bluetooth + {dHash({0x0c45, 0x8101}), avattoH100Mapping}, // Avatto H100, August WP200 + }; + + // check for device id + const auto dit = map.find(dHash(dId)); + if (dit != map.cend()) + { + // check for key event sequence + const auto& kesMap = dit->second; + const auto kit = kesMap.find(eHash(die)); + if (kit != kesMap.cend()) { + return kit->second; + } + } + + static const QString notFound; + return notFound; +} +} // end namespace KeyName \ No newline at end of file diff --git a/src/device-key-lookup.h b/src/device-key-lookup.h new file mode 100644 index 00000000..25020975 --- /dev/null +++ b/src/device-key-lookup.h @@ -0,0 +1,13 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md +#pragma once + +#include "device-defs.h" +#include "deviceinput.h" + +#include + +namespace KeyName +{ + const QString& lookup(const DeviceId& dId, const DeviceInputEvent& die); +} // end namespace KeyName diff --git a/src/deviceinput.cc b/src/deviceinput.cc index 570d4863..faf18df6 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -565,7 +565,7 @@ struct InputMapper::Impl InputMapConfig m_config; bool m_recordingMode = false; - ReservedInputs m_reservedInputs; + SpecialMoveInputs m_specialMoveInputs; }; // ------------------------------------------------------------------------------------------------- @@ -799,7 +799,7 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) } // ------------------------------------------------------------------------------------------------- -void InputMapper::addEvents(KeyEvent key_event) +void InputMapper::addEvents(const KeyEvent& key_event) { if (key_event.empty()) { addEvents({}, 0); } @@ -808,14 +808,17 @@ void InputMapper::addEvents(KeyEvent key_event) return ie; }; - // If key_event do not have SYN event at end, add SYN event before sending to inputMapper. - if (key_event.back().type != EV_SYN) { key_event.emplace_back(EV_SYN, SYN_REPORT, 0); } + // // Check if key_event does have SYN event at end + const bool hasLastSYN = (key_event.back().type == EV_SYN); std::vector events; - events.reserve(key_event.size()); + events.reserve(key_event.size() + ((!hasLastSYN) ? 1 : 0)); for (const auto& dev_input_event : key_event) { events.emplace_back(to_input_event(dev_input_event)); } + + if (!hasLastSYN) { events.emplace_back(input_event{{}, EV_SYN, SYN_REPORT, 0}); } + addEvents(events.data(), events.size()); } @@ -854,9 +857,15 @@ const InputMapConfig& InputMapper::configuration() const } // ------------------------------------------------------------------------------------------------- -InputMapper::ReservedInputs& InputMapper::specialInputs() +const InputMapper::SpecialMoveInputs& InputMapper::specialMoveInputs() +{ + return impl->m_specialMoveInputs; +} + +// ------------------------------------------------------------------------------------------------- +void InputMapper::setSpecialMoveInputs(SpecialMoveInputs moveInputs) { - return impl->m_reservedInputs; + impl->m_specialMoveInputs = std::move(moveInputs); } // ------------------------------------------------------------------------------------------------- @@ -864,22 +873,20 @@ namespace SpecialKeys { // ------------------------------------------------------------------------------------------------- // Functions that provide all special event sequences for a device. -// Currently, special event seqences are only defined for -// Logitech Spotlight device. Please note that all special key event -// sequences are not necessarily be move type Key Seqeuce. +// Currently, special event seqences are only defined for the Logitech Spotlight device. // Move type Key Sequences for the device are stored in -// InputMapper::Impl::m_reservedInputs by SubHidppConnection::updateDeviceFlags. +// InputMapper::Impl::m_specialMoveInputs by SubHidppConnection::updateDeviceFlags. const std::map& keyEventSequenceMap() { static const std::map keyMap { {Key::NextHold, {InputMapper::tr("Next Hold"), - makeSpecialKeyEventSequence(to_integral(Key::NextHold))}}, + KeyEventSequence{{{EV_KEY, to_integral(Key::NextHold), 1}}}}}, {Key::BackHold, {InputMapper::tr("Back Hold"), - makeSpecialKeyEventSequence(to_integral(Key::BackHold))}}, + KeyEventSequence{{{EV_KEY, to_integral(Key::BackHold), 1}}}}}, {Key::NextHoldMove, {InputMapper::tr("Next Hold Move"), - makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove)), true}}, + makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove)) }}, {Key::BackHoldMove, {InputMapper::tr("Back Hold Move"), - makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove)), true}}, + makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove))}}, }; return keyMap; } @@ -896,4 +903,20 @@ const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key) return notFound; } +// ------------------------------------------------------------------------------------------------- +const SpecialKeyEventSeqInfo& logitechSpotlightHoldMove(const KeyEventSequence& inputSequence) +{ + const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); + for (const auto& key : {SpecialKeys::Key::BackHoldMove, SpecialKeys::Key::NextHoldMove}) + { + const auto it = specialKeysMap.find(key); + if (it != specialKeysMap.cend() && it->second.keyEventSeq == inputSequence) { + return it->second; + } + } + + static const SpecialKeyEventSeqInfo notFound; + return notFound; +} + } // end namespace SpecialKeys diff --git a/src/deviceinput.h b/src/deviceinput.h index 3feadb22..7ed4bff7 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -83,22 +83,19 @@ QDebug operator<<(QDebug debug, const KeyEvent &ke); // such a way that they cannot interfere with other valid input events from the device. namespace SpecialKeys { - constexpr uint16_t range = 0x0f00; // 0x0f00 - 0x0fff - constexpr uint16_t userRange = 0x0e00; // 0x0e00 - 0x0eff - enum class Key : uint16_t { - NextHold = 0x0e10, // must be in SpecialKeys user range - BackHold = 0x0e11, // must be in SpecialKeys user range - NextHoldMove = 0x0ff0, // must be in SpecialKeys range - BackHoldMove = 0x0ff1, // must be in SpecialKeys range + NextHold = 0x0e10, + BackHold = 0x0e11, + NextHoldMove = 0x0ff0, + BackHoldMove = 0x0ff1, }; struct SpecialKeyEventSeqInfo { QString name; KeyEventSequence keyEventSeq; - bool isMoveEvent = false; }; + const SpecialKeyEventSeqInfo& logitechSpotlightHoldMove(const KeyEventSequence& inputSequence); const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key); const std::map& keyEventSequenceMap(); } @@ -298,7 +295,7 @@ class InputMapper : public QObject // input_events = complete sequence including SYN event void addEvents(const struct input_event input_events[], size_t num); - void addEvents(KeyEvent key_events); + void addEvents(const KeyEvent& key_events); bool recordingMode() const; void setRecordingMode(bool recording); @@ -306,8 +303,9 @@ class InputMapper : public QObject int keyEventInterval() const; void setKeyEventInterval(int interval); - using ReservedInputs = std::vector; - ReservedInputs& specialInputs(); + using SpecialMoveInputs = std::vector; + const SpecialMoveInputs& specialMoveInputs(); + void setSpecialMoveInputs(SpecialMoveInputs moveInputs); std::shared_ptr virtualDevice() const; bool hasVirtualDevice() const; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 0573c8dc..6cc57b54 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.cc @@ -171,7 +171,7 @@ QWidget* DevicesWidget::createInputMapperWidget(Settings* settings, Spotlight* / intervalLayout->addWidget(intervalUnitLbl); const auto tblView = new InputMapConfigView(imWidget); - const auto imModel = new InputMapConfigModel(m_inputMapper, imWidget); + const auto imModel = new InputMapConfigModel(m_inputMapper, currentDeviceId(), imWidget); if (m_inputMapper) { imModel->setConfiguration(m_inputMapper->configuration()); } tblView->setModel(imModel); @@ -183,11 +183,13 @@ QWidget* DevicesWidget::createInputMapperWidget(Settings* settings, Spotlight* / updateImWidget(); connect(this, &DevicesWidget::currentDeviceChanged, this, - [this, imModel, intervalSb, updateImWidget=std::move(updateImWidget)](){ + [this, imModel, intervalSb, updateImWidget=std::move(updateImWidget)](const DeviceId& dId) + { imModel->setInputMapper(m_inputMapper); if (m_inputMapper) { intervalSb->setValue(m_inputMapper->keyEventInterval()); imModel->setConfiguration(m_inputMapper->configuration()); + imModel->setDeviceId(dId); } updateImWidget(); }); diff --git a/src/inputmapconfig.cc b/src/inputmapconfig.cc index f860576a..f23a494a 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.cc @@ -16,13 +16,9 @@ namespace { } // end anonymous namespace // ------------------------------------------------------------------------------------------------- -InputMapConfigModel::InputMapConfigModel(QObject* parent) - : InputMapConfigModel(nullptr, parent) -{} - -// ------------------------------------------------------------------------------------------------- -InputMapConfigModel::InputMapConfigModel(InputMapper* im, QObject* parent) +InputMapConfigModel::InputMapConfigModel(InputMapper* im, const DeviceId& dId, QObject* parent) : QAbstractTableModel(parent) + , m_currentDeviceId(dId) , m_inputMapper(im) {} @@ -165,15 +161,7 @@ void InputMapConfigModel::setInputSequence(const QModelIndex& index, const KeyEv ++m_duplicates[kes]; c.deviceSequence = kes; - const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); - const bool isSpecialMoveInput = std::any_of(specialKeysMap.cbegin(), specialKeysMap.cend(), - [&c](const auto& specialKeyInfo){ - if (c.deviceSequence == specialKeyInfo.second.keyEventSeq) { - return specialKeyInfo.second.isMoveEvent; - } - return false; - } - ); + const bool isSpecialMoveInput = !SpecialKeys::logitechSpotlightHoldMove(c.deviceSequence).name.isEmpty(); const bool isMoveAction = (c.action->type() == Action::Type::ScrollHorizontal @@ -292,6 +280,18 @@ void InputMapConfigModel::setConfiguration(const InputMapConfig& config) endResetModel(); } +// ------------------------------------------------------------------------------------------------- +const DeviceId& InputMapConfigModel::deviceId() const +{ + return m_currentDeviceId; +} + +// ------------------------------------------------------------------------------------------------- +void InputMapConfigModel::setDeviceId(const DeviceId& dId) +{ + m_currentDeviceId = dId; +} + // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::updateDuplicates() { diff --git a/src/inputmapconfig.h b/src/inputmapconfig.h index 6044e4a3..4f375dd9 100644 --- a/src/inputmapconfig.h +++ b/src/inputmapconfig.h @@ -2,6 +2,7 @@ // - See LICENSE.md and README.md #pragma once +#include "device-defs.h" #include "deviceinput.h" #include @@ -31,8 +32,7 @@ class InputMapConfigModel : public QAbstractTableModel enum Roles { InputSeqRole = Qt::UserRole + 1, ActionTypeRole, NativeSeqRole }; enum Columns { InputSeqCol = 0, ActionTypeCol, ActionCol, ColumnsCount}; - InputMapConfigModel(QObject* parent = nullptr); - InputMapConfigModel(InputMapper* im, QObject* parent = nullptr); + InputMapConfigModel(InputMapper* im, const DeviceId& dId, QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; @@ -54,10 +54,15 @@ class InputMapConfigModel : public QAbstractTableModel InputMapConfig configuration() const; void setConfiguration(const InputMapConfig& config); + const DeviceId& deviceId() const; + void setDeviceId(const DeviceId& dId); + private: void configureInputMapper(); void removeConfigItemRows(int fromRow, int toRow); void updateDuplicates(); + + DeviceId m_currentDeviceId; QPointer m_inputMapper; QVector m_configItems; std::map m_duplicates; diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 8e4c62de..0513c05c 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -3,10 +3,10 @@ #include "inputseqedit.h" +#include "device-key-lookup.h" #include "deviceinput.h" #include "inputmapconfig.h" #include "logging.h" -#include "common-input-seq.h" #include #include @@ -41,17 +41,20 @@ namespace { // ----------------------------------------------------------------------------------------------- int drawKeyEvent(int startX, QPainter& p, const QStyleOption& option, const KeyEvent& ke, - bool buttonTap = false) + const DeviceId& dId, bool buttonTap = false) { if (ke.empty()) { return 0; } static auto const pressChar = QChar(0x2193); // ↓ static auto const releaseChar = QChar(0x2191); // ↑ + const auto& die = (ke.back().code != SYN_REPORT) ? ke.back() : ke.front(); + const auto& lookupName = KeyName::lookup(dId, die); + // TODO Some devices (e.g. August WP 200) have buttons that send a key combination // (modifiers + key) - this is ignored completely right now. const auto text = QString("[%1%2%3") - .arg(ke.back().code != SYN_REPORT ? ke.back().code : ke.front().code, 0, 16) + .arg(lookupName.isEmpty() ? QString("%1").arg(die.code, 0, 16) : lookupName) .arg(buttonTap ? pressChar : ke.back().value ? pressChar : releaseChar) .arg(buttonTap ? "" : "]"); @@ -87,7 +90,8 @@ namespace { // ----------------------------------------------------------------------------------------------- int drawKeyEventSequence(int startX, QPainter& p, const QStyleOption& option, - const KeyEventSequence& kes, bool drawEmptyPlaceholder = true) + const KeyEventSequence& kes, const DeviceId& dId, + bool drawEmptyPlaceholder = true) { if (kes.empty()) { @@ -112,7 +116,7 @@ namespace { return false; }(); - sequenceWidth += drawKeyEvent(startX + sequenceWidth, p, option, *it, isTap); + sequenceWidth += drawKeyEvent(startX + sequenceWidth, p, option, *it, dId, isTap); } return sequenceWidth; @@ -142,13 +146,9 @@ namespace { } // end anonymous namespace // ------------------------------------------------------------------------------------------------- -InputSeqEdit::InputSeqEdit(QWidget* parent) - : InputSeqEdit(nullptr, parent) -{} - -// ------------------------------------------------------------------------------------------------- -InputSeqEdit::InputSeqEdit(InputMapper* im, QWidget* parent) +InputSeqEdit::InputSeqEdit(InputMapper* im, const DeviceId& dId, QWidget* parent) : QWidget(parent) + , m_deviceId(dId) { setInputMapper(im); @@ -219,11 +219,11 @@ void InputSeqEdit::paintEvent(QPaintEvent* /* paintEvent */) if (m_recordedSequence.empty()) { drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")); } else { - drawKeyEventSequence(xPos, p, option, m_recordedSequence, false); + drawKeyEventSequence(xPos, p, option, m_recordedSequence, m_deviceId, false); } } else { - drawKeyEventSequence(xPos, p, option, m_inputSequence); + drawKeyEventSequence(xPos, p, option, m_inputSequence, m_deviceId); } } @@ -312,7 +312,7 @@ void InputSeqEdit::setInputMapper(InputMapper* im) { if (m_inputMapper == im) { return; } - auto removeIm = [this](){ + const auto removeIm = [this](){ if (m_inputMapper) { m_inputMapper->disconnect(this); this->disconnect(m_inputMapper); @@ -410,64 +410,13 @@ void InputSeqDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; const auto& keySeq = imModel->configData(index).deviceSequence; - const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); - const auto& commonKeysInfo = CommonKeyEventSeqInfo::commonInputSeq; - - // Separate out events of type EV_KEY only - const auto keySeqOnlyEV_KEY = [keySeq](){ - KeyEventSequence kes_comparision; - for (auto ke: keySeq) { - KeyEvent temp_ke; - for (auto k: ke){ - if (k.type == EV_KEY) temp_ke.emplace_back(k); - } - kes_comparision.emplace_back(temp_ke); - } - return kes_comparision; - }(); - - const auto it = std::find_if(specialKeysMap.cbegin(), specialKeysMap.cend(), - [&keySeq](const auto& specialKeyInfo){ - return (keySeq == specialKeyInfo.second.keyEventSeq); - } - ); + const auto& holdMoveEvent = SpecialKeys::logitechSpotlightHoldMove(keySeq); - if (it != specialKeysMap.cend()) - { - drawPlaceHolderText(xPos, *painter, option, it->second.name, false); + if (!holdMoveEvent.name.isEmpty()) { + drawPlaceHolderText(xPos, *painter, option, holdMoveEvent.name, false); } - else - { - size_t i = 0, old_i = 0; - int xPosition = xPos; - while (i < keySeqOnlyEV_KEY.size()) - { - for (auto& kes: commonKeysInfo) { - if (kes.keyEventSeq.size() > keySeqOnlyEV_KEY.size() - i) continue; - auto KeqSeqPart = KeyEventSequence(keySeqOnlyEV_KEY.begin()+i, - keySeqOnlyEV_KEY.begin()+i+kes.keyEventSeq.size()); - if (KeqSeqPart == kes.keyEventSeq) { - i += KeqSeqPart.size(); - xPosition += drawPlaceHolderText(xPosition, *painter, option, kes.name, false); - break; - } - } - if (old_i == i) { - if (i+1 < keySeqOnlyEV_KEY.size() && isButtonTap(keySeqOnlyEV_KEY.at(i), keySeqOnlyEV_KEY.at(i+1))) { - xPosition += drawKeyEventSequence(xPosition, *painter, option, {keySeqOnlyEV_KEY.at(i), keySeqOnlyEV_KEY.at(i+1)}); - i += 2; - } - else - { - xPosition += drawKeyEventSequence(xPosition, *painter, option, {keySeqOnlyEV_KEY.at(i)}); - i++; - } - } - if (i < keySeqOnlyEV_KEY.size()) { - xPosition += drawPlaceHolderText(xPosition, *painter, option, ", ", false); - } - old_i = i; - } + else { + drawKeyEventSequence(xPos, *painter, option, keySeq, imModel->deviceId()); } if (option.state & QStyle::State_HasFocus) { @@ -505,7 +454,7 @@ QWidget* InputSeqDelegate::createEditor(QWidget* parent, if (const auto imModel = qobject_cast(index.model())) { if (imModel->inputMapper()) { imModel->inputMapper()->setRecordingMode(false); } - auto *editor = new InputSeqEdit(imModel->inputMapper(), parent); + auto *editor = new InputSeqEdit(imModel->inputMapper(), imModel->deviceId(), parent); connect(editor, &InputSeqEdit::editingFinished, this, &InputSeqDelegate::commitAndCloseEditor); if (imModel->inputMapper()) { imModel->inputMapper()->setRecordingMode(true); } return editor; @@ -570,38 +519,37 @@ void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* { if (!index.isValid() || !model) { return; } - const auto& specialInputs = model->inputMapper()->specialInputs(); - if (!specialInputs.empty()) + const auto& specialMoveInputs = model->inputMapper()->specialMoveInputs(); + if (!specialMoveInputs.empty()) { auto* const menu = new QMenu(parent); - for (const auto& button : specialInputs) { - if (button.isMoveEvent) { - const auto qaction = menu->addAction(button.name); - connect(qaction, &QAction::triggered, this, [model, index, button](){ - model->setInputSequence(index, button.keyEventSeq); - const auto& currentItem = model->configData(index); - if (!currentItem.action) { - model->setItemActionType(index, Action::Type::ScrollVertical); - } - else + for (const auto& input : specialMoveInputs) + { + const auto qaction = menu->addAction(input.name); + connect(qaction, &QAction::triggered, this, [model, index, inputSeq=input.keyEventSeq](){ + model->setInputSequence(index, inputSeq); + const auto& currentItem = model->configData(index); + if (!currentItem.action) { + model->setItemActionType(index, Action::Type::ScrollVertical); + } + else + { + switch (currentItem.action->type()) { - switch (currentItem.action->type()) - { - case Action::Type::ScrollHorizontal: // [[fallthrough]]; - case Action::Type::ScrollVertical: // [[fallthrough]]; - case Action::Type::VolumeControl: { - // scrolling and volume control allowed for special input - break; - } - default: { - model->setItemActionType(index, Action::Type::ScrollVertical); - break; - } + case Action::Type::ScrollHorizontal: // [[fallthrough]]; + case Action::Type::ScrollVertical: // [[fallthrough]]; + case Action::Type::VolumeControl: { + // scrolling and volume control allowed for special input + break; + } + default: { + model->setItemActionType(index, Action::Type::ScrollVertical); + break; } } - }); - } + } + }); } menu->exec(globalPos); diff --git a/src/inputseqedit.h b/src/inputseqedit.h index c19fe32c..c19635e8 100644 --- a/src/inputseqedit.h +++ b/src/inputseqedit.h @@ -2,6 +2,7 @@ // - See LICENSE.md and README.md #pragma once +#include "device-defs.h" #include "deviceinput.h" #include @@ -17,14 +18,11 @@ class InputSeqEdit : public QWidget Q_OBJECT public: - InputSeqEdit(QWidget* parent = nullptr); - InputSeqEdit(InputMapper* im, QWidget* parent = nullptr); + InputSeqEdit(InputMapper* im, const DeviceId& dId, QWidget* parent = nullptr); ~InputSeqEdit(); QSize sizeHint() const override; - void setInputMapper(InputMapper* im); - const KeyEventSequence& inputSequence() const; void setInputSequence(const KeyEventSequence& is); @@ -41,6 +39,8 @@ class InputSeqEdit : public QWidget static int drawEmptyIndicator(int startX, QPainter& p, const QStyleOption& option); protected: + void setInputMapper(InputMapper* im); + void paintEvent(QPaintEvent* e) override; void mouseDoubleClickEvent(QMouseEvent* e) override; void keyPressEvent(QKeyEvent* e) override; @@ -49,6 +49,7 @@ class InputSeqEdit : public QWidget QStyleOptionFrame styleOption() const; private: + DeviceId m_deviceId; InputMapper* m_inputMapper = nullptr; KeyEventSequence m_inputSequence; KeyEventSequence m_recordedSequence; diff --git a/src/spotlight.cc b/src/spotlight.cc index 6589fae9..3633a04a 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -36,8 +36,6 @@ namespace { // Hold button state. Very much Logitech Spotlight specific. struct HoldButtonStatus { - - void setButtonsPressed(bool nextPressed, bool backPressed) { if (!m_nextPressed && nextPressed) { @@ -466,14 +464,18 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext; const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack; - if (!m_holdButtonStatus->nextPressed() && isNextPressed) { - for (auto ke: SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold).keyEventSeq) { + if (!m_holdButtonStatus->nextPressed() && isNextPressed) + { + const auto& nextHold = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold); + for (const auto& ke: nextHold.keyEventSeq) { connection->inputMapper()->addEvents(ke); } } - if (!m_holdButtonStatus->backPressed() && isBackPressed) { - for (auto ke: SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold).keyEventSeq) { + if (!m_holdButtonStatus->backPressed() && isBackPressed) + { + const auto& backHold = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold); + for (const auto& ke: backHold.keyEventSeq) { connection->inputMapper()->addEvents(ke); } } @@ -501,7 +503,7 @@ void Spotlight::registerForNotifications(SubHidppConnection* connection) const int x = intcast(msg[5]); const int y = intcast(msg[7]); - static const auto getReducedParam = [](int param) -> int{ + static const auto getReducedParam = [](int param) -> int { constexpr int divider = 5; constexpr int minimum = 5; constexpr int maximum = 10; From cffdd5f4ef309b983ef028e34de3774b982fe9a0 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Tue, 3 May 2022 09:02:05 +0200 Subject: [PATCH 088/110] Add missing include --- src/device.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/device.h b/src/device.h index 1c515859..1ce24fee 100644 --- a/src/device.h +++ b/src/device.h @@ -7,6 +7,7 @@ #include "devicescan.h" +#include #include #include From b6aa545da9ef23c9d767ffa54f6492e5fbd28b8c Mon Sep 17 00:00:00 2001 From: Sid Roberts Date: Thu, 26 May 2022 07:51:35 +0900 Subject: [PATCH 089/110] Added missing `` include. --- src/devicescan.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/devicescan.cc b/src/devicescan.cc index 2cc2b4f4..32f1cc2d 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -3,6 +3,8 @@ #include "devicescan.h" +#include + #include #include #include From 25d0d98f252189644fcd0542a3eab8891f8bce59 Mon Sep 17 00:00:00 2001 From: TheAssassin Date: Wed, 1 Feb 2023 19:54:03 +0100 Subject: [PATCH 090/110] Add support for August LP310 --- README.md | 1 + devices.conf | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 7853e3f8..0fba05e8 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,7 @@ Besides the _Logitech Spotlight_, the following devices are currently supported * AVATTO H100 / August WP200 _(0c45:8101)_ * August LP315 _(2312:863d)_ * AVATTO i10 Pro _(2571:4109)_ +* August LP310 _(69a7:9803)_ #### Compile Time diff --git a/devices.conf b/devices.conf index bd0d0c3b..e43ce85b 100644 --- a/devices.conf +++ b/devices.conf @@ -10,3 +10,4 @@ 0x2571, 0x4109, usb, AVATTO i10 Pro 0x17ef, 0x60d9, usb, Lenovo ThinkPad X1 Presenter Mouse 0x17ef, 0x60db, bt, Lenovo ThinkPad X1 Presenter Mouse +0x69a7, 0x9803, usb, August LP310 From 323532fcd420f761b885e1a7c68161789aa73dc1 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Thu, 3 Nov 2022 08:45:38 +0100 Subject: [PATCH 091/110] Use lowercased desktop entry template file name Rename the desktop entry template file from "Projecteur.desktop.in" to "projecteur.desktop.in". While, to my knowledge, uppercase letters are valid anywhere in these file names, it is much more common to either write the whole name lowercased (like blender.desktop) or use the reverse DNS notation (where the last part of the name often has some mixed case, like org.octave.Octave.desktop). It seems CMakeLists.txt was already setup to call the final file "projecteur.desktop", so this patch would just make the names more consistent. --- CMakeLists.txt | 4 ++-- .../{Projecteur.desktop.in => projecteur.desktop.in} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename cmake/templates/{Projecteur.desktop.in => projecteur.desktop.in} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b7a317cc..1c4cef86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,7 +195,7 @@ add_translation_update_task("projecteur" "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_ # Add target with non-source files for convenience when using IDEs like QtCreator and others add_custom_target(non-sources SOURCES README.md LICENSE.md doc/CHANGELOG.md devices.conf src/extra-devices.cc.in 55-projecteur.rules.in - cmake/templates/Projecteur.desktop.in) + cmake/templates/projecteur.desktop.in) # Install #--------------------------------------------------------------------------------------------------- @@ -285,7 +285,7 @@ get_target_property(VERSION_STRING projecteur VERSION_STRING) get_target_property(VERSION_DATE_MONTH_YEAR projecteur VERSION_DATE_MONTH_YEAR) set(HOMEPAGE "https://github.com/jahnf/Projecteur") -configure_file("${TMPLDIR}/Projecteur.desktop.in" "projecteur.desktop" @ONLY) +configure_file("${TMPLDIR}/projecteur.desktop.in" "projecteur.desktop" @ONLY) install(FILES "${OUTDIR}/projecteur.desktop" DESTINATION share/applications/) # Configure man page and gzip it. diff --git a/cmake/templates/Projecteur.desktop.in b/cmake/templates/projecteur.desktop.in similarity index 100% rename from cmake/templates/Projecteur.desktop.in rename to cmake/templates/projecteur.desktop.in From 6c5bce39073de3cfaf4548b0d3aa76aa4ff28536 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 18 Feb 2023 17:22:30 +0100 Subject: [PATCH 092/110] ci: add fedora-37 build --- .github/workflows/ci-build.yml | 6 +++--- docker/Dockerfile.fedora-37 | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 docker/Dockerfile.fedora-37 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 685f9fc6..bb184234 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,11 +16,11 @@ jobs: matrix: docker_tag: - archlinux - - fedora-30 - fedora-31 - fedora-32 - fedora-33 - fedora-34 + - fedora-37 - debian-stretch - debian-buster - debian-bullseye @@ -189,9 +189,9 @@ jobs: ["ubuntu-20.04"]="ubuntu/focal" ["ubuntu-21.04"]="ubuntu/hirsute" \ ["opensuse-15.1"]="opensuse/15.1" ["opensuse-15.2"]="opensuse/15.2" \ ["opensuse-15.3"]="opensuse/15.3" ["centos-8"]="el/8" \ - ["fedora-30"]="fedora/30" ["fedora-31"]="fedora/31" \ + ["fedora-31"]="fedora/31" \ ["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" \ - ["fedora-34"]="fedora/34" ) + ["fedora-34"]="fedora/34" ["fedora-37"]="fedora/37" ) export DISTRO=${distromap[${{ matrix.docker_tag }}]} echo PKGTYPE=$PKG_TYPE echo DISTRO=$DISTRO diff --git a/docker/Dockerfile.fedora-37 b/docker/Dockerfile.fedora-37 new file mode 100644 index 00000000..fcd8e8a6 --- /dev/null +++ b/docker/Dockerfile.fedora-37 @@ -0,0 +1,20 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM fedora:37 + +RUN mkdir /build +RUN dnf -y install --setopt=install_weak_deps=False --best \ + cmake \ + udev \ + gcc-c++ \ + tar \ + make \ + git \ + qt5-qtdeclarative-devel \ + pkg-config \ + rpm-build \ + qt5-linguist \ + qt5-qtx11extras-devel \ + libusbx-devel + From 75d2c907066cd814633a42df2934e0ac423e602d Mon Sep 17 00:00:00 2001 From: Jahn Date: Sat, 18 Feb 2023 17:24:32 +0100 Subject: [PATCH 093/110] ci: update github action versions --- .github/workflows/ci-build.yml | 12 ++++++------ .github/workflows/codeql-analysis.yml | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index bb184234..f4d3ab02 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -54,7 +54,7 @@ jobs: # =================================================================================== # ---------- Checkout and build inside docker container ---------- - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - run: | export BRANCH=${GITHUB_REF/refs\/heads\//} echo Detected branch: ${BRANCH} @@ -112,14 +112,14 @@ jobs: # ---------- Upload artifacts to github ---------- - name: Upload source-pkg artifact to github if: startsWith(matrix.docker_tag, 'archlinux') - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: source-package path: ${{ env.src_pkg_artifact }} - name: Upload version-info to github if: startsWith(matrix.docker_tag, 'archlinux') - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: version-info path: | @@ -127,7 +127,7 @@ jobs: ./version-branch - name: Upload binary package artifact to github - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ matrix.docker_tag }}-package path: ${{ env.dist_pkg_artifact }} @@ -220,7 +220,7 @@ jobs: steps: - name: Get version-info - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: version-info @@ -233,7 +233,7 @@ jobs: DO_UPLOAD=$(( [ "master" = "$BRANCH" ] || [ "develop" = "$BRANCH" ] ) && echo true || echo false) echo "DO_UPLOAD=${DO_UPLOAD}" >> $GITHUB_ENV - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 if: env.DO_UPLOAD == 'true' with: path: artifacts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 531ff467..bf65ec46 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,8 +30,8 @@ jobs: # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + # - run: git checkout HEAD^2 + # if: ${{ github.event_name == 'pull_request' }} - name: Configure and build Qt moc cpps run: | @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: queries: +security-and-quality @@ -52,4 +52,4 @@ jobs: make -j2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From cf185cee800f567069c2a3bc28fd08206036fd2c Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 29 Apr 2022 15:58:07 +0200 Subject: [PATCH 094/110] ci: add Ubuntu 22.04 docker file and ci build --- .github/workflows/ci-build.yml | 2 ++ docker/Dockerfile.ubuntu-22.04 | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 docker/Dockerfile.ubuntu-22.04 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index f4d3ab02..73799351 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -28,6 +28,7 @@ jobs: - ubuntu-20.04 - ubuntu-20.10 - ubuntu-21.04 + - ubuntu-22.04 - opensuse-15.0 - opensuse-15.1 - opensuse-15.2 @@ -187,6 +188,7 @@ jobs: declare -A distromap=( ["debian-stretch"]="debian/stretch" ["debian-buster"]="debian/buster" \ ["debian-bullseye"]="debian/bullseye" ["ubuntu-18.04"]="ubuntu/bionic" \ ["ubuntu-20.04"]="ubuntu/focal" ["ubuntu-21.04"]="ubuntu/hirsute" \ + ["ubuntu-22.04"]="ubuntu/jammy" \ ["opensuse-15.1"]="opensuse/15.1" ["opensuse-15.2"]="opensuse/15.2" \ ["opensuse-15.3"]="opensuse/15.3" ["centos-8"]="el/8" \ ["fedora-31"]="fedora/31" \ diff --git a/docker/Dockerfile.ubuntu-22.04 b/docker/Dockerfile.ubuntu-22.04 new file mode 100644 index 00000000..638840ad --- /dev/null +++ b/docker/Dockerfile.ubuntu-22.04 @@ -0,0 +1,23 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM ubuntu:22.04 + +RUN apt-get update && mkdir /build +RUN DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + g++ \ + make \ + cmake \ + udev \ + git \ + pkg-config \ + qtdeclarative5-dev \ + qttools5-dev-tools \ + qttools5-dev \ + libqt5x11extras5-dev \ + libusb-1.0-0-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN git config --global --add safe.directory /source From 4562569a3568410b1d013f43c2e638a574425510 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 19 Feb 2023 19:09:48 +0100 Subject: [PATCH 095/110] ci: fix auto-generated version fetch all git history with checkout --- .github/workflows/ci-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 73799351..1c6cd1ee 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -56,6 +56,10 @@ jobs: # =================================================================================== # ---------- Checkout and build inside docker container ---------- - uses: actions/checkout@v3 + with: + # unfortunately, currently we need all the history for a valid auto generated version + fetch-depth: 0 + - run: | export BRANCH=${GITHUB_REF/refs\/heads\//} echo Detected branch: ${BRANCH} From 23505b9f50fecae7e54f7bc112c3a74776478263 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 19 Feb 2023 19:37:26 +0100 Subject: [PATCH 096/110] ci: fix fedora-37 version generation --- docker/Dockerfile.fedora-37 | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile.fedora-37 b/docker/Dockerfile.fedora-37 index fcd8e8a6..a926c031 100644 --- a/docker/Dockerfile.fedora-37 +++ b/docker/Dockerfile.fedora-37 @@ -18,3 +18,4 @@ RUN dnf -y install --setopt=install_weak_deps=False --best \ qt5-qtx11extras-devel \ libusbx-devel +RUN git config --global --add safe.directory /source From ca9e6afd8742bf8dd9d8581e306f0f4826b97608 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 28 Feb 2023 18:23:54 +0100 Subject: [PATCH 097/110] fix: repair broken uinput support on newer Linux distros --- .github/workflows/codeql-analysis.yml | 2 +- 55-projecteur.rules.in | 4 +- src/aboutdlg.cc | 14 ++- src/device.cc | 7 +- src/device.h | 4 +- src/deviceinput.cc | 128 +++++++++++++++++++++----- src/deviceinput.h | 11 ++- src/nativekeyseqedit.cc | 12 +-- src/spotlight.cc | 21 +++-- src/spotlight.h | 3 +- src/virtualdevice.cc | 57 +++++++++--- src/virtualdevice.h | 18 +++- 12 files changed, 215 insertions(+), 66 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bf65ec46..388ee417 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,7 +22,7 @@ jobs: qt5-default libqt5x11extras5-dev - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/55-projecteur.rules.in b/55-projecteur.rules.in index dacce1f6..af461319 100644 --- a/55-projecteur.rules.in +++ b/55-projecteur.rules.in @@ -17,6 +17,6 @@ SUBSYSTEMS=="hid", KERNELS=="0005:046D:B503.*", MODE="0660", TAG+="uaccess" # Additional supported Bluetooth devices @EXTRA_BLUETOOTH_UDEV_RULES@ -# Rules for uninput: Essential for creating a virtual input device that -# Projecteur use for forwarding device events to the system after grabbing it +# Rules for uinput: Essential for creating a virtual input device that +# Projecteur uses to forward device events to the system after grabbing it KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput" diff --git a/src/aboutdlg.cc b/src/aboutdlg.cc index 12736671..167e084f 100644 --- a/src/aboutdlg.cc +++ b/src/aboutdlg.cc @@ -18,7 +18,8 @@ #include namespace { - // ------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------- + /// Contributor (name, github_name, email, url) struct Contributor { explicit Contributor(const QString& name = {}, const QString& github_name = {}, @@ -27,8 +28,10 @@ namespace { QString toHtml() const { - auto html = QString("%1").arg(name.isEmpty() ? QString("%1").arg(github_name) - : name); + auto html = QString("%1").arg(name.isEmpty() + ? QString("%1").arg(github_name) + : name); + if (email.size()) { html += QString(" <%1>").arg(email); } @@ -48,7 +51,7 @@ namespace { QString url; }; - // ------------------------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------------------- QString getContributorsHtml() { static std::vector contributors = @@ -66,6 +69,9 @@ namespace { Contributor("Stuart Prescott", "llimeht"), Contributor("Crista Renouard", "Lumnicence"), Contributor("freddii", "freddii"), + Contributor("Matthias Blümel", "Blaimi"), + Contributor("Grzegorz Szymaszek", "gszy"), + Contributor("TheAssassin", "TheAssassin"), }; static std::mt19937 g(std::random_device{}()); diff --git a/src/device.cc b/src/device.cc index a8a4778d..32db26a9 100644 --- a/src/device.cc +++ b/src/device.cc @@ -63,10 +63,11 @@ const char* toString(ConnectionMode cm, bool withClass) // ------------------------------------------------------------------------------------------------- DeviceConnection::DeviceConnection(const DeviceId& id, const QString& name, - std::shared_ptr vdev) + std::shared_ptr vmouse, + std::shared_ptr vkeyboard) : m_deviceId(id) , m_deviceName(name) - , m_inputMapper(std::make_shared(std::move(vdev))) + , m_inputMapper(std::make_shared(std::move(vmouse), std::move(vkeyboard))) { } @@ -245,7 +246,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan: connection->m_details.grabbed = [&dc, evfd, &sd]() { // Grab device inputs if a virtual device exists. - if (dc.inputMapper()->virtualDevice()) + if (dc.inputMapper()->hasVirtualDevice()) { const int res = ioctl(evfd, EVIOCGRAB, 1); if (res == 0) { return true; } diff --git a/src/device.h b/src/device.h index 1ce24fee..b23d0742 100644 --- a/src/device.h +++ b/src/device.h @@ -29,7 +29,9 @@ class DeviceConnection : public QObject Q_OBJECT public: - DeviceConnection(const DeviceId& id, const QString& name, std::shared_ptr vdev); + DeviceConnection(const DeviceId& id, const QString& name, + std::shared_ptr vmouse, std::shared_ptr vkeyboard); + ~DeviceConnection(); const auto& deviceName() const { return m_deviceName; } diff --git a/src/deviceinput.cc b/src/deviceinput.cc index faf18df6..7fc5a36e 100644 --- a/src/deviceinput.cc +++ b/src/deviceinput.cc @@ -58,6 +58,29 @@ namespace { return KeyEventSequence{std::move(pressed)}; }; + + // ----------------------------------------------------------------------------------------------- + bool isMouseEvent(const input_event* input_events, size_t num) + { + if (num < 2) { + // no events, or single SYN event + return false; + } + + auto const& ev = [&]() -> input_event const& { + if (input_events[0].type == EV_MSC) { + return input_events[1]; + } + return input_events[0]; + }(); + + if (ev.type == EV_KEY && ev.code >= BTN_MISC && ev.code < KEY_OK) { + return true; + } + + return false; + } + } // end anonymous namespace // ------------------------------------------------------------------------------------------------- @@ -547,16 +570,26 @@ const char* toString(Action::Type at, bool withClass) // ------------------------------------------------------------------------------------------------- struct InputMapper::Impl { - Impl(InputMapper* parent, std::shared_ptr vdev); + Impl(InputMapper* parent, + std::shared_ptr virtualMouse, + std::shared_ptr virtualKeybaord); void sequenceTimeout(); void resetState(); void record(const struct input_event input_events[], size_t num); void emitNativeKeySequence(const NativeKeySequence& ks); void execAction(const std::shared_ptr& action, DeviceKeyMap::Result r); + bool hasVirtualDevices() const; + + void forwardEvents(const struct input_event input_events[], size_t num); + void forwardEvents(const std::vector& input_events); InputMapper* m_parent = nullptr; - std::shared_ptr m_vdev; // can be a nullptr if application is started without uinput + + // virtual devices can be empty shared_ptr's if app is started without uinput + std::shared_ptr m_vmouse; + std::shared_ptr m_vkeyboard; + QTimer* m_seqTimer = nullptr; DeviceKeyMap m_keymap; @@ -569,9 +602,12 @@ struct InputMapper::Impl }; // ------------------------------------------------------------------------------------------------- -InputMapper::Impl::Impl(InputMapper* parent, std::shared_ptr vdev) +InputMapper::Impl::Impl(InputMapper* parent + , std::shared_ptr virtualMouse + , std::shared_ptr virtualKeyboard) : m_parent(parent) - , m_vdev(std::move(vdev)) + , m_vmouse(std::move(virtualMouse)) + , m_vkeyboard(std::move(virtualKeyboard)) , m_seqTimer(new QTimer(parent)) { constexpr int defaultSequenceIntervalMs = 250; @@ -580,6 +616,12 @@ InputMapper::Impl::Impl(InputMapper* parent, std::shared_ptr vdev connect(m_seqTimer, &QTimer::timeout, parent, [this](){ sequenceTimeout(); }); } +// ------------------------------------------------------------------------------------------------- +bool InputMapper::Impl::hasVirtualDevices() const +{ + return (m_vmouse && m_vkeyboard); +} + // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::execAction(const std::shared_ptr& action, DeviceKeyMap::Result r) { @@ -612,9 +654,9 @@ void InputMapper::Impl::sequenceTimeout() if (m_lastState.first == DeviceKeyMap::Result::Valid) { // Last input event was part of a valid key sequence, but timeout hit // So we emit our stored event so far to the virtual device - if (m_vdev && !m_events.empty()) + if (hasVirtualDevices() && !m_events.empty()) { - m_vdev->emitEvents(m_events); + forwardEvents(m_events); } resetState(); } @@ -625,9 +667,10 @@ void InputMapper::Impl::sequenceTimeout() { execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); } - else if (m_vdev && !m_events.empty()) + else if (hasVirtualDevices() && !m_events.empty()) { - m_vdev->emitEvents(m_events); + // TODO differentiate between mouse and keyboard events + forwardEvents(m_events); m_events.resize(0); } resetState(); @@ -644,7 +687,7 @@ void InputMapper::Impl::resetState() // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::emitNativeKeySequence(const NativeKeySequence& ks) { - if (!m_vdev) { return; } + if (!m_vkeyboard) { return; } std::vector events; events.reserve(5); // up to 3 modifier keys + 1 key + 1 syn event @@ -653,7 +696,7 @@ void InputMapper::Impl::emitNativeKeySequence(const NativeKeySequence& ks) for (const auto& ie : ke) { events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); } - m_vdev->emitEvents(events); + m_vkeyboard->emitEvents(events); events.resize(0); } } @@ -671,25 +714,67 @@ void InputMapper::Impl::record(const struct input_event input_events[], size_t n } // ------------------------------------------------------------------------------------------------- +void InputMapper::Impl::forwardEvents(const std::vector& input_events) +{ + forwardEvents(input_events.data(), input_events.size()); +} + // ------------------------------------------------------------------------------------------------- -InputMapper::InputMapper(std::shared_ptr virtualDevice, QObject* parent) +void InputMapper::Impl::forwardEvents(const struct input_event input_events[], size_t num) +{ + input_event const* beg = input_events; + input_event const* end = input_events + num; + + auto predicate = [](input_event const& e){ + return e.type == EV_SYN; + }; + + // handle each part separated by a SYN event + input_event const* syn = std::find_if(beg, end, predicate); + + while (syn != end) { + auto const len = std::distance(beg, syn) + 1; + + if (isMouseEvent(beg, len)) { + m_vmouse->emitEvents(beg, len); + } else { + m_vkeyboard->emitEvents(beg, len); + } + + beg = syn + 1; + syn = std::find_if(beg, end, predicate); + } +} + +// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- +InputMapper::InputMapper( + std::shared_ptr virtualMouse + , std::shared_ptr virtualKeyboard + , QObject* parent) : QObject(parent) - , impl(std::make_unique(this, std::move(virtualDevice))) + , impl(std::make_unique(this, std::move(virtualMouse), std::move(virtualKeyboard))) {} // ------------------------------------------------------------------------------------------------- InputMapper::~InputMapper() = default; // ------------------------------------------------------------------------------------------------- -std::shared_ptr InputMapper::virtualDevice() const +std::shared_ptr InputMapper::virtualMouse() const { - return impl->m_vdev; + return impl->m_vmouse; +} + +// ------------------------------------------------------------------------------------------------- +std::shared_ptr InputMapper::virtualKeyboard() const +{ + return impl->m_vkeyboard; } // ------------------------------------------------------------------------------------------------- bool InputMapper::hasVirtualDevice() const { - return !!(impl->m_vdev); + return impl->hasVirtualDevices(); } // ------------------------------------------------------------------------------------------------- @@ -728,12 +813,12 @@ void InputMapper::setKeyEventInterval(int interval) // ------------------------------------------------------------------------------------------------- void InputMapper::addEvents(const input_event* input_events, size_t num) { - if (num == 0 || (!impl->m_vdev)) { return; } + if (num == 0 || (!hasVirtualDevice())) { return; } // If no key mapping is configured ... if (!impl->m_recordingMode && !impl->m_keymap.hasConfig()) { - // ... forward events to virtual device if it exists... - impl->m_vdev->emitEvents(input_events, num); + // ... forward events to virtual device + impl->forwardEvents(input_events, num); return; } @@ -774,7 +859,8 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) if (res == DeviceKeyMap::Result::Miss) { // key sequence miss, send all buffered events so far impl->m_seqTimer->stop(); - impl->m_vdev->emitEvents(impl->m_events); + + impl->forwardEvents(impl->m_events); impl->resetState(); } @@ -785,7 +871,7 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) impl->execAction(pos->action, res); } else { - impl->m_vdev->emitEvents(impl->m_events); + impl->forwardEvents(impl->m_events); } impl->resetState(); @@ -808,7 +894,7 @@ void InputMapper::addEvents(const KeyEvent& key_event) return ie; }; - // // Check if key_event does have SYN event at end + // Check if key_event does have SYN event at end const bool hasLastSYN = (key_event.back().type == EV_SYN); std::vector events; diff --git a/src/deviceinput.h b/src/deviceinput.h index 7ed4bff7..71c4547a 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.h @@ -79,7 +79,7 @@ QDebug operator<<(QDebug debug, const KeyEvent &ke); // Some inputs from Logitech Spotlight device (like Next Hold and Back Hold events) are not a valid // input event (input_event in linux/input.h) in a conventional sense. They are communicated // via HID++ messages from the device. Using the input mapper we need to -// reserve some KeyEventSequence for theese events. These KeyEventSequence should be designed in +// reserve some KeyEventSequence for these events. These KeyEventSequence should be designed in // such a way that they cannot interfere with other valid input events from the device. namespace SpecialKeys { @@ -288,7 +288,11 @@ class InputMapper : public QObject Q_OBJECT public: - InputMapper(std::shared_ptr virtualDevice, QObject* parent = nullptr); + InputMapper( + std::shared_ptr virtualMouse, + std::shared_ptr virtualKeyboard, + QObject* parent = nullptr); + ~InputMapper(); void resetState(); // Reset any stored sequence state. @@ -307,7 +311,8 @@ class InputMapper : public QObject const SpecialMoveInputs& specialMoveInputs(); void setSpecialMoveInputs(SpecialMoveInputs moveInputs); - std::shared_ptr virtualDevice() const; + std::shared_ptr virtualMouse() const; + std::shared_ptr virtualKeyboard() const; bool hasVirtualDevice() const; void setConfiguration(const InputMapConfig& config); diff --git a/src/nativekeyseqedit.cc b/src/nativekeyseqedit.cc index 46d1e9e0..1246fd7a 100644 --- a/src/nativekeyseqedit.cc +++ b/src/nativekeyseqedit.cc @@ -208,7 +208,7 @@ bool NativeKeySeqEdit::event(QEvent* e) return QWidget::event(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::recordKeyPressEvent(QKeyEvent* e) { int key = m_lastKey = e->key(); @@ -261,7 +261,7 @@ void NativeKeySeqEdit::recordKeyPressEvent(QKeyEvent* e) } } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::keyPressEvent(QKeyEvent* e) { if (!recording()) @@ -285,7 +285,7 @@ void NativeKeySeqEdit::keyPressEvent(QKeyEvent* e) recordKeyPressEvent(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::keyReleaseEvent(QKeyEvent* e) { if (recording()) @@ -312,14 +312,14 @@ void NativeKeySeqEdit::keyReleaseEvent(QKeyEvent* e) QWidget::keyReleaseEvent(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::focusOutEvent(QFocusEvent* e) { setRecording(false); QWidget::focusOutEvent(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- int NativeKeySeqEdit::getQtModifiers(Qt::KeyboardModifiers state) { int result = 0; @@ -331,7 +331,7 @@ int NativeKeySeqEdit::getQtModifiers(Qt::KeyboardModifiers state) return result; } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- uint16_t NativeKeySeqEdit::getNativeModifiers(const std::set& modifiersPressed) { using Modifier = NativeKeySequence::Modifier; diff --git a/src/spotlight.cc b/src/spotlight.cc index 3633a04a..20bd96fc 100644 --- a/src/spotlight.cc +++ b/src/spotlight.cc @@ -90,7 +90,10 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) }); if (m_options.enableUInput) { - m_virtualDevice = VirtualDevice::create(); + m_virtualMouseDevice = VirtualDevice::create( + VirtualDevice::Type::Mouse, "Projecteur_virtual_mouse"); + m_virtualKeyDevice = VirtualDevice::create( + VirtualDevice::Type::Keyboard, "Projecteur_virtual_keyboard"); } else { logInfo(device) << tr("Virtual device initialization was skipped."); @@ -174,7 +177,8 @@ int Spotlight::connectDevices() { auto& dc = m_deviceConnections[dev.id]; if (!dc) { - dc = std::make_shared(dev.id, dev.getName(), m_virtualDevice); + dc = std::make_shared( + dev.id, dev.getName(), m_virtualMouseDevice, m_virtualKeyDevice); } const bool anyConnectedBefore = anySpotlightDeviceConnected(); @@ -283,7 +287,7 @@ int Spotlight::connectDevices() } else if (action->type() == Action::Type::ScrollHorizontal || action->type() == Action::Type::ScrollVertical) { - if (!m_virtualDevice) { return; } + if (!m_virtualMouseDevice) { return; } const int param = (action->type() == Action::Type::ScrollHorizontal) ? static_cast(action.get())->param @@ -293,18 +297,18 @@ int Spotlight::connectDevices() { const uint16_t wheelCode = (action->type() == Action::Type::ScrollHorizontal) ? REL_HWHEEL : REL_WHEEL; const std::vector scrollInputEvents = {{{}, EV_REL, wheelCode, param}, {{}, EV_SYN, SYN_REPORT, 0},}; - m_virtualDevice->emitEvents(scrollInputEvents); + m_virtualMouseDevice->emitEvents(scrollInputEvents); } } else if (action->type() == Action::Type::VolumeControl) { - if (!m_virtualDevice) { return; } + if (!m_virtualMouseDevice) { return; } auto param = static_cast(action.get())->param; uint16_t keyCode = (param > 0)? KEY_VOLUMEUP: KEY_VOLUMEDOWN; const std::vector curVolInputEvents = {{{}, EV_KEY, keyCode, 1}, {{}, EV_SYN, SYN_REPORT, 0}, {{}, EV_KEY, keyCode, 0}, {{}, EV_SYN, SYN_REPORT, 0},}; - if (param) { m_virtualDevice->emitEvents(curVolInputEvents); } + if (param) { m_virtualMouseDevice->emitEvents(curVolInputEvents); } } }); @@ -423,7 +427,10 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) } m_activeTimer->start(); - if (m_virtualDevice) { m_virtualDevice->emitEvents(buf.data(), buf.pos()); } + if (m_virtualMouseDevice) { + // forward events to virtual mouse device + m_virtualMouseDevice->emitEvents(buf.data(), buf.pos()); + } } else { // Forward events to input mapper for the device diff --git a/src/spotlight.h b/src/spotlight.h index ea04e15a..d77c9eef 100644 --- a/src/spotlight.h +++ b/src/spotlight.h @@ -76,7 +76,8 @@ class Spotlight : public QObject, public async::Async QTimer* m_connectionTimer = nullptr; QTimer* m_holdMoveEventTimer = nullptr; bool m_spotActive = false; - std::shared_ptr m_virtualDevice; + std::shared_ptr m_virtualMouseDevice; + std::shared_ptr m_virtualKeyDevice; Settings* m_settings = nullptr; std::unique_ptr m_holdButtonStatus; }; diff --git a/src/virtualdevice.cc b/src/virtualdevice.cc index ecdd4570..be163757 100644 --- a/src/virtualdevice.cc +++ b/src/virtualdevice.cc @@ -7,34 +7,47 @@ #include #include +#include #include #include LOGGING_CATEGORY(virtualdevice, "virtualdevice") +// KEY_MACRO1 is only defined in newer linux versions +#ifndef KEY_MACRO1 +#define KEY_MACRO1 0x290 +#endif + namespace { class VirtualDevice_ : public QObject {}; // for i18n and logging } // end anonymous namespace struct VirtualDevice::Token {}; -VirtualDevice::VirtualDevice(Token /* token */, int fd) +// ------------------------------------------------------------------------------------------------- +VirtualDevice::VirtualDevice(Token /* token */, int fd, const char* name, const char* sysfs_name) : m_uinpFd(fd) + , m_userName(name) + , m_deviceName(sysfs_name) {} +// ------------------------------------------------------------------------------------------------- VirtualDevice::~VirtualDevice() { if (m_uinpFd >= 0) { ioctl(m_uinpFd, UI_DEV_DESTROY); ::close(m_uinpFd); - logDebug(virtualdevice) << VirtualDevice_::tr("uinput Device Closed"); + logDebug(virtualdevice) + << VirtualDevice_::tr("uinput Device Closed (%1; %2)").arg(m_userName, m_deviceName); } } -// Setup uinput device that can send mouse and keyboard events. -std::shared_ptr VirtualDevice::create(const char* name, +// ------------------------------------------------------------------------------------------------- +// Setup a uinput device that can send mouse or keyboard events. +std::shared_ptr VirtualDevice::create(Type deviceType, + const char* name, uint16_t virtualVendorId, uint16_t virtualProductId, uint16_t virtualVersionId, @@ -44,14 +57,14 @@ std::shared_ptr VirtualDevice::create(const char* name, if (!fi.exists()) { logWarn(virtualdevice) << VirtualDevice_::tr("File not found: %1").arg(location); logWarn(virtualdevice) << VirtualDevice_::tr("Please check if uinput kernel module is loaded"); - return std::unique_ptr(); + return std::shared_ptr(); } const int fd = ::open(location, O_WRONLY | O_NDELAY); if (fd < 0) { logWarn(virtualdevice) << VirtualDevice_::tr("Unable to open: %1").arg(location); logWarn(virtualdevice) << VirtualDevice_::tr("Please check if current user has write access"); - return std::unique_ptr(); + return std::shared_ptr(); } struct uinput_user_dev uinp {}; @@ -67,14 +80,30 @@ std::shared_ptr VirtualDevice::create(const char* name, ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_EVBIT, EV_REL); - // Set all rel event code bits on virtual device + // Set all relative event code bits on virtual device for (int i = 0; i < REL_CNT; ++i) { ioctl(fd, UI_SET_RELBIT, i); } - // Set all key code bits on virtual device - for (int i = 1; i < KEY_CNT; ++i) { - ioctl(fd, UI_SET_KEYBIT, i); + // Thank's to Matthias Blümel / https://github.com/Blaimi + // for the detailed investigation on the uinput issue on newer + // Linux distributions. + // See https://github.com/jahnf/Projecteur/issues/175#issuecomment-1432112896 + + if (deviceType == Type::Mouse) { + // Set key code bits for a virtual mouse + for (int i = BTN_MISC; i < KEY_OK; ++i) { + ioctl(fd, UI_SET_KEYBIT, i); + } + } else if (deviceType == Type::Keyboard) { + // Set key code bits for a virtual keyboard + for (int i = 1; i < BTN_MISC; ++i) { + ioctl(fd, UI_SET_KEYBIT, i); + } + for (int i = KEY_OK; i < KEY_MACRO1; ++i) { + ioctl(fd, UI_SET_KEYBIT, i); + } + // will set key bits from i = KEY_MACRO1 to i < KEY_CNT also work? } // Create input device into input sub-system @@ -90,11 +119,13 @@ std::shared_ptr VirtualDevice::create(const char* name, char sysfs_device_name[16]{}; ioctl(fd, UI_GET_SYSNAME(sizeof(sysfs_device_name)), sysfs_device_name); logInfo(virtualdevice) << VirtualDevice_::tr("Created uinput device: %1") - .arg(QString("/sys/devices/virtual/input/%1").arg(sysfs_device_name)); + .arg(QString("%1; /sys/devices/virtual/input/%2") + .arg(name, sysfs_device_name)); - return std::make_shared(Token{}, fd); + return std::make_shared(Token{}, fd, name, sysfs_device_name); } +// ------------------------------------------------------------------------------------------------- void VirtualDevice::emitEvents(const struct input_event input_events[], size_t num) { if (!num) { return; } @@ -107,8 +138,8 @@ void VirtualDevice::emitEvents(const struct input_event input_events[], size_t n } } +// ------------------------------------------------------------------------------------------------- void VirtualDevice::emitEvents(const std::vector& events) { emitEvents(events.data(), events.size()); } - diff --git a/src/virtualdevice.h b/src/virtualdevice.h index e5b73f38..f70d2761 100644 --- a/src/virtualdevice.h +++ b/src/virtualdevice.h @@ -7,26 +7,36 @@ # pragma once +#include + #include #include #include -// Device that can act as virtual keyboard and mouse +/// Device that can act as virtual keyboard or mouse class VirtualDevice { private: struct Token; int m_uinpFd = -1; + QString m_userName; + QString m_deviceName; public: - // Return a VirtualDevice shared_ptr or an empty shared_ptr if the creation fails. - static std::shared_ptr create(const char* name = "Projecteur_input_device", + enum class Type { + Mouse, + Keyboard + }; + + /// Return a VirtualDevice shared_ptr or an empty shared_ptr if the creation fails. + static std::shared_ptr create(Type deviceType, + const char* name = "Projecteur_input_device", uint16_t virtualVendorId = 0xfeed, uint16_t virtualProductId = 0xc0de, uint16_t virtualVersionId = 1, const char* location = "/dev/uinput"); - explicit VirtualDevice(Token, int fd); + VirtualDevice(Token, int fd, const char* name, const char* sysfs_name); ~VirtualDevice(); void emitEvents(const struct input_event[], size_t num); From 88434b4f96c8485491779c0430174f553f48f162 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Fri, 13 Oct 2023 06:42:27 +0200 Subject: [PATCH 098/110] ci: set new target release to 0.10.0 --- CMakeLists.txt | 5 +++-- cmake/modules/GitVersion.cmake | 16 ++++++++++++++-- doc/CHANGELOG.md | 10 ++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c4cef86..f6350b30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,10 +172,11 @@ target_compile_definitions(projecteur PRIVATE # Update this information - the version numbers and the version type. # VERSION_TYPE must be either 'release' or 'develop' set_target_properties(projecteur PROPERTIES - VERSION_MAJOR 1 - VERSION_MINOR 0 + VERSION_MAJOR 0 + VERSION_MINOR 10 VERSION_PATCH 0 VERSION_TYPE develop + VERSION_DISTANCE_OFFSET 200 ) add_version_info(projecteur "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/cmake/modules/GitVersion.cmake b/cmake/modules/GitVersion.cmake index de6f43d0..f32e82d8 100644 --- a/cmake/modules/GitVersion.cmake +++ b/cmake/modules/GitVersion.cmake @@ -66,6 +66,9 @@ function(get_version_info prefix directory) set(${prefix}_VERSION_ISDIRTY 0 PARENT_SCOPE) set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}" PARENT_SCOPE) set(${prefix}_VERSION_DATE_MONTH_YEAR "" PARENT_SCOPE) + if("${${prefix}_VERSION_DISTANCE_OFFSET}" STREQUAL "") + set(${prefix}_VERSION_DISTANCE_OFFSET 0) + endif() if("${${prefix}_OR_VERSION_MAJOR}" STREQUAL "") set(${prefix}_OR_VERSION_MAJOR 0) @@ -248,7 +251,10 @@ function(get_version_info prefix directory) endif() set(${prefix}_VERSION_FLAG ${${prefix}_VERSION_FLAG} PARENT_SCOPE) - set(${prefix}_VERSION_DISTANCE ${${prefix}_VERSION_DISTANCE} PARENT_SCOPE) + + math(EXPR CALCULATED_GIT_DISTANCE "${${prefix}_VERSION_DISTANCE}+${${prefix}_VERSION_DISTANCE_OFFSET}") + set(${prefix}_VERSION_DISTANCE ${CALCULATED_GIT_DISTANCE}) + set(${prefix}_VERSION_DISTANCE ${CALCULATED_GIT_DISTANCE} PARENT_SCOPE) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD RESULT_VARIABLE resultSH @@ -352,6 +358,7 @@ function(add_version_info_custom_prefix target prefix directory) set(VERSION_PATCH 0) set(VERSION_FLAG unknown) set(VERSION_DISTANCE 0) + set(VERSION_DISTANCE_OFFSET 0) set(VERSION_SHORTHASH unknown) set(VERSION_FULLHASH unknown) set(VERSION_STRING "0.0-unknown.0") @@ -378,7 +385,12 @@ function(add_version_info_custom_prefix target prefix directory) if(TARGET_VTYPE) set(${prefix}_FALLBACK_VERSION_TYPE ${TARGET_VTYPE}) endif() - + get_target_property(TARGET_VDIST_OFFSET ${target} VERSION_DISTANCE_OFFSET) + if(TARGET_VDIST_OFFSET) + set(VERSION_DISTANCE_OFFSET ${TARGET_VDIST_OFFSET}) + endif() + set(${prefix}_VERSION_DISTANCE_OFFSET ${VERSION_DISTANCE_OFFSET}) + include(ArchiveVersionInfo_${prefix} OPTIONAL RESULT_VARIABLE ARCHIVE_VERSION_PRESENT) if(ARCHIVE_VERSION_PRESENT AND ${prefix}_VERSION_SUCCESS) message(STATUS "Info: Version information from archive file.") diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index dffada31..9e6ce831 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,16 +1,18 @@ # Projecteur Changelog -## v1.0 +## v0.10.0 ### Changes/Updates: - Logitech Spotlight Bluetooth vibration & hidraw support ([#140][p140]); - Logitech Spotlight Scrolling and Audio Volume functionality ([#85][i85]); -- Added automated builds for Fedora 34, Debian 11 (Bullseye) and OpenSUSE 15.3 ([#148][i148]) +- Add automated builds for Fedora 34, Debian 11 (Bullseye) and OpenSUSE 15.3 ([#148][i148]) +- Add automated builds for Fedora 37 and 38 / OpenSUSE 15.4 and 15.5 +- Add automated builds for Ubuntu 23.04 and Debian Bookworm - Bug fix for crash when closing the about dialog. -Many thanks to *[@mayanksuman][c-mayanksuman]* for Logitech Bluetooth, Scrolling and Audio volume -support. +Many thanks to *[@mayanksuman][c-mayanksuman]* for Logitech Bluetooth, Scrolling +and Audio volume support. [p140]: https://github.com/jahnf/Projecteur/pull/140 [i85]: https://github.com/jahnf/Projecteur/issues/85 From f7c6f1df50d6cc79861e6981467eeced786c8980 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 13 Oct 2023 09:41:52 +0200 Subject: [PATCH 099/110] ci: fix master version generation --- cmake/modules/GitVersion.cmake | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmake/modules/GitVersion.cmake b/cmake/modules/GitVersion.cmake index f32e82d8..61311e87 100644 --- a/cmake/modules/GitVersion.cmake +++ b/cmake/modules/GitVersion.cmake @@ -321,7 +321,7 @@ function(get_version_info prefix directory) if(NOT ${${prefix}_VERSION_PATCH} EQUAL 0) set(VERSION_STRING "${VERSION_STRING}.${${prefix}_VERSION_PATCH}") endif() - if(NOT ON_MASTER OR NOT ${${prefix}_VERSION_DISTANCE} EQUAL 0) + if(NOT ON_MASTER OR NOT ${${prefix}_VERSION_DISTANCE} EQUAL ${${prefix}_VERSION_DISTANCE_OFFSET}) set(VERSION_STRING "${VERSION_STRING}-${${prefix}_VERSION_FLAG}") set(VERSION_STRING_FULL "${VERSION_STRING_FULL}-${${prefix}_VERSION_FLAG}") endif() @@ -329,7 +329,10 @@ function(get_version_info prefix directory) set(VERSION_STRING "${VERSION_STRING}.") set(VERSION_STRING_FULL "${VERSION_STRING_FULL}.") endif() - if(NOT ON_MASTER OR (NOT ON_MASTER AND NOT ${${prefix}_VERSION_DISTANCE} EQUAL 0)) + if(NOT ON_MASTER OR (NOT ON_MASTER AND NOT ${${prefix}_VERSION_DISTANCE} EQUAL ${${prefix}_VERSION_DISTANCE_OFFSET})) + set(VERSION_STRING "${VERSION_STRING}${${prefix}_VERSION_DISTANCE}") + set(VERSION_STRING_FULL "${VERSION_STRING_FULL}${${prefix}_VERSION_DISTANCE}") + elseif(ON_MASTER AND NOT ${${prefix}_VERSION_DISTANCE} EQUAL ${${prefix}_VERSION_DISTANCE_OFFSET}) set(VERSION_STRING "${VERSION_STRING}${${prefix}_VERSION_DISTANCE}") set(VERSION_STRING_FULL "${VERSION_STRING_FULL}${${prefix}_VERSION_DISTANCE}") endif() @@ -390,7 +393,7 @@ function(add_version_info_custom_prefix target prefix directory) set(VERSION_DISTANCE_OFFSET ${TARGET_VDIST_OFFSET}) endif() set(${prefix}_VERSION_DISTANCE_OFFSET ${VERSION_DISTANCE_OFFSET}) - + include(ArchiveVersionInfo_${prefix} OPTIONAL RESULT_VARIABLE ARCHIVE_VERSION_PRESENT) if(ARCHIVE_VERSION_PRESENT AND ${prefix}_VERSION_SUCCESS) message(STATUS "Info: Version information from archive file.") From 2d09072f53c4156b35389bfe8796bd1fb8c76624 Mon Sep 17 00:00:00 2001 From: Jahn F Date: Thu, 12 Oct 2023 10:05:54 +0200 Subject: [PATCH 100/110] feat: add norwii wireless presenter #199 --- devices.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/devices.conf b/devices.conf index e43ce85b..fb3a1a08 100644 --- a/devices.conf +++ b/devices.conf @@ -11,3 +11,4 @@ 0x17ef, 0x60d9, usb, Lenovo ThinkPad X1 Presenter Mouse 0x17ef, 0x60db, bt, Lenovo ThinkPad X1 Presenter Mouse 0x69a7, 0x9803, usb, August LP310 +0x3243, 0x0122, usb, Norwii Wireless Presenter From 419d5933f564c651b1c7f1495978e104bf0464c0 Mon Sep 17 00:00:00 2001 From: Jahn Date: Thu, 12 Oct 2023 19:20:36 +0200 Subject: [PATCH 101/110] ci: add fedora-38 build --- .github/workflows/ci-build.yml | 3 ++- cmake/modules/LinuxDistributionInfo.cmake | 2 +- cmake/modules/LinuxPackaging.cmake | 2 +- docker/Dockerfile.fedora-38 | 21 +++++++++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 docker/Dockerfile.fedora-38 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 1c6cd1ee..a4ade606 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -21,6 +21,7 @@ jobs: - fedora-33 - fedora-34 - fedora-37 + - fedora-38 - debian-stretch - debian-buster - debian-bullseye @@ -197,7 +198,7 @@ jobs: ["opensuse-15.3"]="opensuse/15.3" ["centos-8"]="el/8" \ ["fedora-31"]="fedora/31" \ ["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" \ - ["fedora-34"]="fedora/34" ["fedora-37"]="fedora/37" ) + ["fedora-34"]="fedora/34" ["fedora-37"]="fedora/37" ["fedora-38"]="fedora/38" ) export DISTRO=${distromap[${{ matrix.docker_tag }}]} echo PKGTYPE=$PKG_TYPE echo DISTRO=$DISTRO diff --git a/cmake/modules/LinuxDistributionInfo.cmake b/cmake/modules/LinuxDistributionInfo.cmake index 4663c370..5b9df22b 100644 --- a/cmake/modules/LinuxDistributionInfo.cmake +++ b/cmake/modules/LinuxDistributionInfo.cmake @@ -1,5 +1,5 @@ # This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.6) # Try to get the Linux distribution and version as a string (host system) # When cross compiling this function won't work to get the target distribution. diff --git a/cmake/modules/LinuxPackaging.cmake b/cmake/modules/LinuxPackaging.cmake index 75aff50b..29bd9e60 100644 --- a/cmake/modules/LinuxPackaging.cmake +++ b/cmake/modules/LinuxPackaging.cmake @@ -1,5 +1,5 @@ # This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.6) include(LinuxDistributionInfo) set(_LinuxPackaging_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}") diff --git a/docker/Dockerfile.fedora-38 b/docker/Dockerfile.fedora-38 new file mode 100644 index 00000000..997e5d74 --- /dev/null +++ b/docker/Dockerfile.fedora-38 @@ -0,0 +1,21 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM fedora:38 + +RUN mkdir /build +RUN dnf -y install --setopt=install_weak_deps=False --best \ + cmake \ + udev \ + gcc-c++ \ + tar \ + make \ + git \ + qt5-qtdeclarative-devel \ + pkg-config \ + rpm-build \ + qt5-linguist \ + qt5-qtx11extras-devel \ + libusbx-devel + +RUN git config --global --add safe.directory /source From 0c27cfff58390a31489784697e90ab9d50cd5a89 Mon Sep 17 00:00:00 2001 From: Jahn Date: Thu, 12 Oct 2023 19:39:52 +0200 Subject: [PATCH 102/110] ci: add opensuse 15.4 and 15.5 --- .github/workflows/ci-build.yml | 5 ++++- docker/Dockerfile.opensuse-15.4 | 21 +++++++++++++++++++++ docker/Dockerfile.opensuse-15.5 | 21 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 docker/Dockerfile.opensuse-15.4 create mode 100644 docker/Dockerfile.opensuse-15.5 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a4ade606..9141abea 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -34,6 +34,8 @@ jobs: - opensuse-15.1 - opensuse-15.2 - opensuse-15.3 + - opensuse-15.4 + - opensuse-15.5 - centos-8 os: - ubuntu-latest @@ -195,7 +197,8 @@ jobs: ["ubuntu-20.04"]="ubuntu/focal" ["ubuntu-21.04"]="ubuntu/hirsute" \ ["ubuntu-22.04"]="ubuntu/jammy" \ ["opensuse-15.1"]="opensuse/15.1" ["opensuse-15.2"]="opensuse/15.2" \ - ["opensuse-15.3"]="opensuse/15.3" ["centos-8"]="el/8" \ + ["opensuse-15.3"]="opensuse/15.3" ["opensuse-15.4"]="opensuse/15.4" \ + ["opensuse-15.5"]="opensuse/15.5" ["centos-8"]="el/8" \ ["fedora-31"]="fedora/31" \ ["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" \ ["fedora-34"]="fedora/34" ["fedora-37"]="fedora/37" ["fedora-38"]="fedora/38" ) diff --git a/docker/Dockerfile.opensuse-15.4 b/docker/Dockerfile.opensuse-15.4 new file mode 100644 index 00000000..f9b8f405 --- /dev/null +++ b/docker/Dockerfile.opensuse-15.4 @@ -0,0 +1,21 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM opensuse/leap:15.4 + +RUN mkdir /build +RUN zypper --non-interactive in --no-recommends \ + pkg-config \ + udev \ + gcc-c++ \ + tar \ + make \ + cmake \ + git \ + wget \ + libqt5-qtdeclarative-devel \ + rpmbuild \ + libqt5-linguist \ + libqt5-qtx11extras-devel \ + libusb-1_0-devel \ + libQt5DBus-devel diff --git a/docker/Dockerfile.opensuse-15.5 b/docker/Dockerfile.opensuse-15.5 new file mode 100644 index 00000000..829cd8c9 --- /dev/null +++ b/docker/Dockerfile.opensuse-15.5 @@ -0,0 +1,21 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM opensuse/leap:15.5 + +RUN mkdir /build +RUN zypper --non-interactive in --no-recommends \ + pkg-config \ + udev \ + gcc-c++ \ + tar \ + make \ + cmake \ + git \ + wget \ + libqt5-qtdeclarative-devel \ + rpmbuild \ + libqt5-linguist \ + libqt5-qtx11extras-devel \ + libusb-1_0-devel \ + libQt5DBus-devel From cdf29cc7efdc56068ef232ffcab04ea46a836088 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 13 Oct 2023 15:03:48 +0200 Subject: [PATCH 103/110] docs: update linux repositories markdown --- README.md | 1 + doc/LinuxRepositories.md | 69 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0fba05e8..45bbf41c 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,7 @@ Besides the _Logitech Spotlight_, the following devices are currently supported * August LP315 _(2312:863d)_ * AVATTO i10 Pro _(2571:4109)_ * August LP310 _(69a7:9803)_ +* Norwii Wireless Presenter _(3243:0122)_ #### Compile Time diff --git a/doc/LinuxRepositories.md b/doc/LinuxRepositories.md index f7bf0345..def5ca00 100644 --- a/doc/LinuxRepositories.md +++ b/doc/LinuxRepositories.md @@ -15,7 +15,7 @@ for all available `projecteur` packages in Debian. ### Ubuntu -Thanks to debian packages, _Projecteur_ is availabed in the official Ubuntu repositories +Thanks to debian packages, _Projecteur_ is available in the official Ubuntu repositories from Ubuntu 20.10 on. See: https://packages.ubuntu.com/search?keywords=projecteur&searchon=names ### Gentoo Linux @@ -79,6 +79,17 @@ curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/c apt-get update ``` +#### Debian Bookworm + +```sh +apt-get install -y debian-keyring +apt-get install -y debian-archive-keyring +apt-get install -y apt-transport-https +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add - +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=debian&codename=bookworm' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list +apt-get update +``` + #### Ubuntu 18.04 ```sh @@ -97,6 +108,24 @@ curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/c apt-get update ``` +#### Ubuntu 22.04 + +```sh +apt-get install -y apt-transport-https +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add - +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=jammy' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list +apt-get update +``` + +#### Ubuntu 23.04 + +```sh +apt-get install -y apt-transport-https +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add - +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=lunar' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list +apt-get update +``` + #### OpenSuse 15.1 ```sh @@ -121,6 +150,22 @@ zypper ar -f '/tmp/jahnf-projecteur-develop.repo' zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source ``` +#### OpenSuse 15.4 + +```sh +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.4' > /tmp/jahnf-projecteur-develop.repo +zypper ar -f '/tmp/jahnf-projecteur-develop.repo' +zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source +``` + +#### OpenSuse 15.5 + +```sh +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.5' > /tmp/jahnf-projecteur-develop.repo +zypper ar -f '/tmp/jahnf-projecteur-develop.repo' +zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source +``` + #### Fedora 31 ```sh @@ -161,9 +206,29 @@ dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo' dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source' ``` -#### CentOS 8 +#### Fedora 37 + +```sh +dnf install yum-utils pygpgme +rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=37' > /tmp/jahnf-projecteur-develop.repo +dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo' +dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source' +``` + +#### Fedora 38 +```sh +dnf install yum-utils pygpgme +rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' +curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=38' > /tmp/jahnf-projecteur-develop.repo +dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo' +dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source' ``` + +#### CentOS 8 + +```sh yum install yum-utils pygpgme rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=el&codename=8' > /tmp/jahnf-projecteur-develop.repo From 964074638ef4e5596fc5cb41dad835ee964a9894 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 13 Oct 2023 14:53:15 +0200 Subject: [PATCH 104/110] ci: add ubuntu 23.04 and debian bookworm --- .github/workflows/ci-build.yml | 7 +++++-- docker/Dockerfile.debian-bookworm | 23 +++++++++++++++++++++++ docker/Dockerfile.ubuntu-23.04 | 23 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 docker/Dockerfile.debian-bookworm create mode 100644 docker/Dockerfile.ubuntu-23.04 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 9141abea..6d0706c1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -25,11 +25,13 @@ jobs: - debian-stretch - debian-buster - debian-bullseye + - debian-bookworm - ubuntu-18.04 - ubuntu-20.04 - ubuntu-20.10 - ubuntu-21.04 - ubuntu-22.04 + - ubuntu-23.04 - opensuse-15.0 - opensuse-15.1 - opensuse-15.2 @@ -193,9 +195,10 @@ jobs: filename=$(basename -- "${{ env.dist_pkg_artifact }}") export PKG_TYPE="${filename##*.}" declare -A distromap=( ["debian-stretch"]="debian/stretch" ["debian-buster"]="debian/buster" \ - ["debian-bullseye"]="debian/bullseye" ["ubuntu-18.04"]="ubuntu/bionic" \ + ["debian-bullseye"]="debian/bullseye" ["debian-bookworm"]="debian/bookworm" \ + ["ubuntu-18.04"]="ubuntu/bionic" \ ["ubuntu-20.04"]="ubuntu/focal" ["ubuntu-21.04"]="ubuntu/hirsute" \ - ["ubuntu-22.04"]="ubuntu/jammy" \ + ["ubuntu-22.04"]="ubuntu/jammy" ["ubuntu-23.04"]="ubuntu/lunar" \ ["opensuse-15.1"]="opensuse/15.1" ["opensuse-15.2"]="opensuse/15.2" \ ["opensuse-15.3"]="opensuse/15.3" ["opensuse-15.4"]="opensuse/15.4" \ ["opensuse-15.5"]="opensuse/15.5" ["centos-8"]="el/8" \ diff --git a/docker/Dockerfile.debian-bookworm b/docker/Dockerfile.debian-bookworm new file mode 100644 index 00000000..705d8777 --- /dev/null +++ b/docker/Dockerfile.debian-bookworm @@ -0,0 +1,23 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM debian:bookworm + +RUN apt-get update && mkdir /build +RUN DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + g++ \ + make \ + cmake \ + udev \ + git \ + pkg-config \ + qtdeclarative5-dev \ + qttools5-dev-tools \ + qttools5-dev \ + libqt5x11extras5-dev \ + libusb-1.0-0-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN git config --global --add safe.directory /source diff --git a/docker/Dockerfile.ubuntu-23.04 b/docker/Dockerfile.ubuntu-23.04 new file mode 100644 index 00000000..5f0c20bf --- /dev/null +++ b/docker/Dockerfile.ubuntu-23.04 @@ -0,0 +1,23 @@ +# Container for building the Projecteur package +# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags + +FROM ubuntu:23.04 + +RUN apt-get update && mkdir /build +RUN DEBIAN_FRONTEND="noninteractive" \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + g++ \ + make \ + cmake \ + udev \ + git \ + pkg-config \ + qtdeclarative5-dev \ + qttools5-dev-tools \ + qttools5-dev \ + libqt5x11extras5-dev \ + libusb-1.0-0-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN git config --global --add safe.directory /source From f4eb508edbbec52c8c3058e222d600e80c90d2d6 Mon Sep 17 00:00:00 2001 From: Jahn Date: Fri, 13 Oct 2023 17:08:04 +0200 Subject: [PATCH 105/110] fix(ci): add safe git directory to opensuse containers --- docker/Dockerfile.opensuse-15.4 | 2 ++ docker/Dockerfile.opensuse-15.5 | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docker/Dockerfile.opensuse-15.4 b/docker/Dockerfile.opensuse-15.4 index f9b8f405..7765cdaf 100644 --- a/docker/Dockerfile.opensuse-15.4 +++ b/docker/Dockerfile.opensuse-15.4 @@ -19,3 +19,5 @@ RUN zypper --non-interactive in --no-recommends \ libqt5-qtx11extras-devel \ libusb-1_0-devel \ libQt5DBus-devel + +RUN git config --global --add safe.directory /source diff --git a/docker/Dockerfile.opensuse-15.5 b/docker/Dockerfile.opensuse-15.5 index 829cd8c9..a2f3a225 100644 --- a/docker/Dockerfile.opensuse-15.5 +++ b/docker/Dockerfile.opensuse-15.5 @@ -19,3 +19,5 @@ RUN zypper --non-interactive in --no-recommends \ libqt5-qtx11extras-devel \ libusb-1_0-devel \ libQt5DBus-devel + +RUN git config --global --add safe.directory /source From 5763302de2fde0c0d65cb149a2df3fec5649fc04 Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 15 Oct 2023 13:16:46 +0200 Subject: [PATCH 106/110] feat: add increase/decrease spot size commands issue: #209 --- cmake/templates/projecteur.bash-completion | 2 +- src/projecteurapp.cc | 44 ++++++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/cmake/templates/projecteur.bash-completion b/cmake/templates/projecteur.bash-completion index 111e87b2..c7ad86ca 100644 --- a/cmake/templates/projecteur.bash-completion +++ b/cmake/templates/projecteur.bash-completion @@ -20,7 +20,7 @@ _projecteur() case "$prev" in "-c") # Auto completion for commands and properties - local commands="quit spot= settings= preset=" + local commands="quit spot= spot.size.adjust= settings= preset=" commands="${commands} spot.size= spot.rotation= spot.shape= spot.shape.square.radius=" commands="${commands} spot.multi-screen= spot.overlay=" commands="${commands} spot.shape.star.points= spot.shape.star.innerradius= spot.shape.ngon.sides=" diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index f1e016e3..cc73981d 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -59,6 +59,7 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio return; } + // don't quit application when last windows (usually preferences dialog) is closed setQuitOnLastWindowClosed(false); QFontDatabase::addApplicationFont(":/icons/projecteur-icons.ttf"); @@ -78,8 +79,8 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio }); const QString desktopEnv = m_linuxDesktop->type() == LinuxDesktop::Type::KDE ? "KDE" : - m_linuxDesktop->type() == LinuxDesktop::Type::Gnome ? "Gnome" - : tr("Unknown"); + m_linuxDesktop->type() == LinuxDesktop::Type::Gnome ? "Gnome" + : tr("Unknown"); logDebug(mainapp) << tr("Qt platform plugin: %1;").arg(QGuiApplication::platformName()) << tr("Desktop Environment: %1;").arg(desktopEnv) @@ -585,13 +586,30 @@ void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) logDebug(cmdserver) << tr("Received quit command."); this->quit(); } + else if (cmdKey == "spot.size.adjust") + { + bool ok = false; + int const sizeAdjust = cmdValue.toInt(&ok); + if (ok) { + logDebug(cmdserver) << tr("Received command spot.size.adjust = %1%2") + .arg(sizeAdjust > 0 ? "+" : "") + .arg(sizeAdjust); + m_settings->setSpotSize(m_settings->spotSize() + sizeAdjust); + } else { + logDebug(cmdserver) << tr("Received invalid value for command spot.size.adjust"); + } + } else if (cmdKey == "spot") { - if (cmdValue.toLower() == "toggle") { + if (cmdValue.isEmpty()) { + logDebug(cmdserver) << tr("Received empty command value for command spot"); + } else if (cmdValue.toLower() == "toggle") { m_spotlight->setSpotActive(!m_spotlight->spotActive()); } else { - const bool active = (cmdValue.toLower() == "on" || cmdValue == "1" || cmdValue.toLower() == "true"); + const bool active = (cmdValue.toLower() == "on" + || cmdValue == "1" + || cmdValue.toLower() == "true"); logDebug(cmdserver) << tr("Received command spot = %1").arg(active); m_spotlight->setSpotActive(active); } @@ -658,18 +676,20 @@ ProjecteurCommandClientApp::ProjecteurCommandClientApp(const QStringList& ipcCom QLocalSocket* const localSocket = new QLocalSocket(this); + auto socketErrorFunc = [this, localSocket](QLocalSocket::LocalSocketError /*socketError*/) { + logError(cmdclient) << tr("Error sending commands: %1", "%1=error message") + .arg(localSocket->errorString()); + localSocket->close(); + QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); + }; + #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - connect(localSocket, &QLocalSocket::errorOccurred, + connect(localSocket, &QLocalSocket::errorOccurred, this, std::move(socketErrorFunc)); #else connect(localSocket, - static_cast(&QLocalSocket::error), + static_cast(&QLocalSocket::error), + this, std::move(socketErrorFunc)); #endif - this, - [this, localSocket](QLocalSocket::LocalSocketError /*socketError*/) { - logError(cmdclient) << tr("Error sending commands: %1", "%1=error message").arg(localSocket->errorString()); - localSocket->close(); - QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); - }); connect(localSocket, &QLocalSocket::connected, [localSocket, &ipcCommands]() { From d28b3af2a33b0c3d704a82fbe58a6adeedf4432e Mon Sep 17 00:00:00 2001 From: Jahn Date: Sun, 15 Oct 2023 16:00:50 +0200 Subject: [PATCH 107/110] feat: add command line vibrate command --- CMakeLists.txt | 53 +++++++++++----------- README.md | 52 ++++++++++++++------- cmake/templates/projecteur.bash-completion | 2 +- src/device-command-helper.cc | 52 +++++++++++++++++++++ src/device-command-helper.h | 24 ++++++++++ src/main.cc | 1 + src/projecteurapp.cc | 39 ++++++++++++++++ src/projecteurapp.h | 7 ++- 8 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 src/device-command-helper.cc create mode 100644 src/device-command-helper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f6350b30..2eb00177 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,32 +108,33 @@ else() endif() add_executable(projecteur - src/main.cc src/enum-helper.h - src/aboutdlg.cc src/aboutdlg.h - src/actiondelegate.cc src/actiondelegate.h - src/colorselector.cc src/colorselector.h - src/device.cc src/device.h - src/device-hidpp.cc src/device-hidpp.h - src/device-key-lookup.cc src/device-key-lookup.h - src/device-vibration.cc src/device-vibration.h - src/deviceinput.cc src/deviceinput.h - src/devicescan.cc src/devicescan.h - src/deviceswidget.cc src/deviceswidget.h - src/hidpp.cc src/hidpp.h - src/linuxdesktop.cc src/linuxdesktop.h - src/iconwidgets.cc src/iconwidgets.h - src/imageitem.cc src/imageitem.h - src/inputmapconfig.cc src/inputmapconfig.h - src/inputseqedit.cc src/inputseqedit.h - src/logging.cc src/logging.h - src/nativekeyseqedit.cc src/nativekeyseqedit.h - src/preferencesdlg.cc src/preferencesdlg.h - src/projecteurapp.cc src/projecteurapp.h - src/runguard.cc src/runguard.h - src/settings.cc src/settings.h - src/spotlight.cc src/spotlight.h - src/spotshapes.cc src/spotshapes.h - src/virtualdevice.h src/virtualdevice.cc + src/main.cc src/enum-helper.h + src/aboutdlg.cc src/aboutdlg.h + src/actiondelegate.cc src/actiondelegate.h + src/colorselector.cc src/colorselector.h + src/device.cc src/device.h + src/device-command-helper.cc src/device-command-helper.h + src/device-hidpp.cc src/device-hidpp.h + src/device-key-lookup.cc src/device-key-lookup.h + src/device-vibration.cc src/device-vibration.h + src/deviceinput.cc src/deviceinput.h + src/devicescan.cc src/devicescan.h + src/deviceswidget.cc src/deviceswidget.h + src/hidpp.cc src/hidpp.h + src/linuxdesktop.cc src/linuxdesktop.h + src/iconwidgets.cc src/iconwidgets.h + src/imageitem.cc src/imageitem.h + src/inputmapconfig.cc src/inputmapconfig.h + src/inputseqedit.cc src/inputseqedit.h + src/logging.cc src/logging.h + src/nativekeyseqedit.cc src/nativekeyseqedit.h + src/preferencesdlg.cc src/preferencesdlg.h + src/projecteurapp.cc src/projecteurapp.h + src/runguard.cc src/runguard.h + src/settings.cc src/settings.h + src/spotlight.cc src/spotlight.h + src/spotshapes.cc src/spotshapes.h + src/virtualdevice.cc src/virtualdevice.h ${RESOURCES}) target_include_directories(projecteur PRIVATE src) diff --git a/README.md b/README.md index 45bbf41c..6464f742 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,39 @@ So here it is: a Linux application for the Logitech Spotlight. ## Table of Contents -* [Motivation](#motivation) -* [Features](#features) -* [Supported Environments](#supported-environments) -* [How it works](#how-it-works) -* [Download](#download) -* [Building](#building) -* [Installation/Running](#installationrunning) - * [Pre-requisites](#pre-requisites) - * [Application Menu](#application-menu) - * [Command Line Interface](#command-line-interface) - * [Scriptability / Keyboard shortcuts](#scriptability) - * [Using Projecteur without a device](#using-projecteur-without-a-device) - * [Device Support](#device-support) - * [Troubleshooting](#troubleshooting) -* [Changelog](#changelog) -* [License](#license) +- [Projecteur](#projecteur) + - [Motivation](#motivation) + - [Table of Contents](#table-of-contents) + - [Features](#features) + - [Screenshots](#screenshots) + - [Planned features](#planned-features) + - [Supported Environments](#supported-environments) + - [How it works](#how-it-works) + - [Button mapping](#button-mapping) + - [Hold Button Mapping for Logitech Spotlight](#hold-button-mapping-for-logitech-spotlight) + - [Download](#download) + - [Building](#building) + - [Requirements](#requirements) + - [Build Example](#build-example) + - [Installation/Running](#installationrunning) + - [Pre-requisites](#pre-requisites) + - [When building Projecteur yourself](#when-building-projecteur-yourself) + - [Application Menu](#application-menu) + - [Command Line Interface](#command-line-interface) + - [Scriptability](#scriptability) + - [Using Projecteur without a device](#using-projecteur-without-a-device) + - [Device Support](#device-support) + - [Compile Time](#compile-time) + - [Runtime](#runtime) + - [Troubleshooting](#troubleshooting) + - [Opaque Spotlight / No Transparency](#opaque-spotlight--no-transparency) + - [Missing System Tray](#missing-system-tray) + - [Zoom is not updated while spotlight is shown](#zoom-is-not-updated-while-spotlight-is-shown) + - [Wayland](#wayland) + - [Wayland Zoom](#wayland-zoom) + - [Device shows as not connected](#device-shows-as-not-connected) + - [Changelog](#changelog) + - [License](#license) ## Features @@ -250,6 +267,9 @@ Example: projecteur -c border=true # Set the border color to red projecteur -c border.color=#ff0000 +# Send a vibrate command to the device with +# intensity=128 and length=0 (only Logitech Spotlight) +projecteur -c vibrate=128,0 ``` While _Projecteur_ does not provide global keyboard shortcuts, command line options diff --git a/cmake/templates/projecteur.bash-completion b/cmake/templates/projecteur.bash-completion index c7ad86ca..718ced85 100644 --- a/cmake/templates/projecteur.bash-completion +++ b/cmake/templates/projecteur.bash-completion @@ -20,7 +20,7 @@ _projecteur() case "$prev" in "-c") # Auto completion for commands and properties - local commands="quit spot= spot.size.adjust= settings= preset=" + local commands="quit spot= spot.size.adjust= settings= preset= vibrate=" commands="${commands} spot.size= spot.rotation= spot.shape= spot.shape.square.radius=" commands="${commands} spot.multi-screen= spot.overlay=" commands="${commands} spot.shape.star.points= spot.shape.star.innerradius= spot.shape.ngon.sides=" diff --git a/src/device-command-helper.cc b/src/device-command-helper.cc new file mode 100644 index 00000000..85ad73df --- /dev/null +++ b/src/device-command-helper.cc @@ -0,0 +1,52 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + +#include "device-command-helper.h" + +#include "device-hidpp.h" +#include "spotlight.h" + +// ------------------------------------------------------------------------------------------------- +DeviceCommandHelper::DeviceCommandHelper(QObject* parent, Spotlight* spotlight) + : QObject(parent), m_spotlight(spotlight) +{ + +} + +// ------------------------------------------------------------------------------------------------- +DeviceCommandHelper::~DeviceCommandHelper() = default; + + +// ------------------------------------------------------------------------------------------------- +bool DeviceCommandHelper::sendVibrateCommand(uint8_t intensity, uint8_t length) +{ + if (m_spotlight.isNull()) { + return false; + } + + for ( auto const& dev : m_spotlight->connectedDevices()) { + if (auto connection = m_spotlight->deviceConnection(dev.id)) { + if (!connection->hasHidppSupport()) { + continue; + } + + for (auto const& subInfo : connection->subDevices()) { + auto const& subConn = subInfo.second; + if (!subConn || !subConn->hasFlags(DeviceFlag::Vibrate)) { + continue; + } + + if (auto hidppConn = std::dynamic_pointer_cast(subConn)) + { + hidppConn->sendVibrateCommand(intensity, length, + [](HidppConnectionInterface::MsgResult, HIDPP::Message&&) { + // logDebug(hid) << tr("Vibrate command returned: %1 (%2)") + // .arg(toString(result)).arg(msg.hex()); + }); + } + } + } + } + + return true; +} diff --git a/src/device-command-helper.h b/src/device-command-helper.h new file mode 100644 index 00000000..00fb34ed --- /dev/null +++ b/src/device-command-helper.h @@ -0,0 +1,24 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md +#pragma once + +#include +#include + +class Spotlight; + +/// Class that offers easy access to device commands with a given Spotlight +/// instance. +class DeviceCommandHelper : public QObject +{ + Q_OBJECT + +public: + explicit DeviceCommandHelper(QObject* parent, Spotlight* spotlight); + virtual ~DeviceCommandHelper(); + + bool sendVibrateCommand(uint8_t intensity, uint8_t length); + +private: + QPointer m_spotlight; +}; diff --git a/src/main.cc b/src/main.cc index fe2985e9..045130b9 100644 --- a/src/main.cc +++ b/src/main.cc @@ -290,6 +290,7 @@ namespace { print() << " settings=[show|hide] " << Main::tr("Show/hide preferences dialog."); if (fullHelp) { print() << " preset=NAME " << Main::tr("Set a preset."); + print() << " vibrate[=I[,L]] " << Main::tr("Send vibrate command to device with intensity,length."); } print() << " quit " << Main::tr("Quit the running instance."); diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index cc73981d..378b3c6e 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.cc @@ -4,6 +4,7 @@ #include "projecteurapp.h" #include "aboutdlg.h" +#include "device-command-helper.h" #include "imageitem.h" #include "linuxdesktop.h" #include "logging.h" @@ -68,6 +69,8 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio m_spotlight = new Spotlight(this, Spotlight::Options{options.enableUInput, options.additionalDevices}, m_settings); + m_deviceCommandHelper = new DeviceCommandHelper(this, m_spotlight); + m_settings->setOverlayDisabled(options.disableOverlay); m_dialog = std::make_unique(m_settings, m_spotlight, options.dialogMinimizeOnly @@ -586,6 +589,42 @@ void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) logDebug(cmdserver) << tr("Received quit command."); this->quit(); } + else if (cmdKey == "vibrate") // with args intensity (0-255), length (0-10) + { + #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + auto const args = cmdValue.split(QLatin1Char(','), Qt::SkipEmptyParts); + #else + auto const args = cmdValue.split(QLatin1Char(','), QString::SkipEmptyParts); + #endif + + std::uint8_t const intensity = [&args]{ + if (args.size() >= 1) { + bool ok = false; + auto intensity = args[0].toInt(&ok); + if (ok) { + return static_cast(qMin(255, qMax(0, intensity))); + } + } + return std::uint8_t{128}; + }(); + + std::uint8_t const length = [&args]{ + if (args.size() >= 2) { + bool ok = false; + auto intensity = args[1].toInt(&ok); + if (ok) { + return static_cast(qMin(10, qMax(0, intensity))); + } + } + return std::uint8_t{0}; + }(); + + logDebug(cmdserver) << tr("Received command vibrate = intensity:%1, length:%2") + .arg(intensity) + .arg(length); + + m_deviceCommandHelper->sendVibrateCommand(intensity, length); + } else if (cmdKey == "spot.size.adjust") { bool ok = false; diff --git a/src/projecteurapp.h b/src/projecteurapp.h index 6a394c83..e3ab04ae 100644 --- a/src/projecteurapp.h +++ b/src/projecteurapp.h @@ -2,7 +2,7 @@ // - See LICENSE.md and README.md #pragma once -#include "spotlight.h" +#include "devicescan.h" #include #include @@ -11,6 +11,7 @@ #include class AboutDialog; +class DeviceCommandHelper; class LinuxDesktop; class PreferencesDialog; class QLocalServer; @@ -20,6 +21,7 @@ class QQmlApplicationEngine; class QQmlComponent; class QSystemTrayIcon; class Settings; +class Spotlight; class ProjecteurApplication : public QApplication { @@ -78,8 +80,9 @@ private slots: std::unique_ptr m_dialog; QPointer m_aboutDialog; QLocalServer* const m_localServer = nullptr; - Spotlight* m_spotlight = nullptr; Settings* m_settings = nullptr; + Spotlight* m_spotlight = nullptr; + DeviceCommandHelper* m_deviceCommandHelper = nullptr; LinuxDesktop* m_linuxDesktop = nullptr; QQmlApplicationEngine* m_qmlEngine = nullptr; QQmlComponent* m_windowQmlComponent = nullptr; From 7e0e2cc9acbac99667b32c989867c1af17d7e3a1 Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 17 Oct 2023 06:46:21 +0200 Subject: [PATCH 108/110] docs: update for spot.size.adjust command --- README.md | 30 ++++++++++++++++-------------- doc/CHANGELOG.md | 4 ++++ src/main.cc | 14 +++++++++----- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6464f742..25367c50 100644 --- a/README.md +++ b/README.md @@ -233,22 +233,24 @@ commands to a running instance of _Projecteur_ and the ability to set properties Usage: projecteur [OPTION]... - -h, --help Show command line usage. - --help-all Show complete command line usage with all properties. - -v, --version Print application version. - -f, --fullversion Print extended version info. - --cfg FILE Set custom config file. - -d, --device-scan Print device-scan results. - -l, --log-level LEVEL Set log level (dbg,inf,wrn,err), default is 'inf'. - --show-dialog Show preferences dialog on start. - -m, --minimize-only Only allow minimizing the preferences dialog. - -D DEVICE Additional accepted device; DEVICE=vendorId:productId - -c COMMAND|PROPERTY Send command/property to a running instance. + -h, --help Show command line usage. + --help-all Show complete command line usage with all properties. + -v, --version Print application version. + -f, --fullversion Print extended version info. + --cfg FILE Set custom config file. + -d, --device-scan Print device-scan results. + -l, --log-level LEVEL Set log level (dbg,inf,wrn,err), default is 'inf'. + --show-dialog Show preferences dialog on start. + -m, --minimize-only Only allow minimizing the preferences dialog. + -D DEVICE Additional accepted device; DEVICE=vendorId:productId + -c COMMAND|PROPERTY Send command/property to a running instance. - spot=[on|off|toggle] Turn spotlight on/off or toggle. - settings=[show|hide] Show/hide preferences dialog. - quit Quit the running instance. + spot=[on|off|toggle] Turn spotlight on/off or toggle. + spot.size.adjust=[+|-]N Increase or decrease spot size by N. + settings=[show|hide] Show/hide preferences dialog. + preset=NAME Set a preset. + quit Quit the running instance. ``` A complete list the properties that can be set via the command line, can be listed with the diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 9e6ce831..f7f2cff2 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -10,6 +10,8 @@ - Add automated builds for Fedora 37 and 38 / OpenSUSE 15.4 and 15.5 - Add automated builds for Ubuntu 23.04 and Debian Bookworm - Bug fix for crash when closing the about dialog. +- Add adjust spot size command ([#209][i209]) +- Add vibrate for the command line ([#202][i202]) Many thanks to *[@mayanksuman][c-mayanksuman]* for Logitech Bluetooth, Scrolling and Audio volume support. @@ -17,6 +19,8 @@ and Audio volume support. [p140]: https://github.com/jahnf/Projecteur/pull/140 [i85]: https://github.com/jahnf/Projecteur/issues/85 [i148]: https://github.com/jahnf/Projecteur/issues/148 +[i209]: https://github.com/jahnf/Projecteur/issues/209 +[i202]: https://github.com/jahnf/Projecteur/issues/202 [c-mayanksuman]: https://github.com/mayanksuman ## v0.9.2 diff --git a/src/main.cc b/src/main.cc index 045130b9..26b452fa 100644 --- a/src/main.cc +++ b/src/main.cc @@ -286,13 +286,17 @@ namespace { } print() << " -c COMMAND|PROPERTY " << commandOption_.description() << std::endl; print() << ""; - print() << " spot=[on|off|toggle] " << Main::tr("Turn spotlight on/off or toggle."); - print() << " settings=[show|hide] " << Main::tr("Show/hide preferences dialog."); + print() << " spot=[on|off|toggle] " << Main::tr("Turn spotlight on/off or toggle."); if (fullHelp) { - print() << " preset=NAME " << Main::tr("Set a preset."); - print() << " vibrate[=I[,L]] " << Main::tr("Send vibrate command to device with intensity,length."); + print() << " preset=NAME " << Main::tr("Set a preset."); + print() << " vibrate[=I[,L]] " << Main::tr("Send vibrate command to device with intensity,length."); + print() << " spot.size.adjust=[+|-]N " << Main::tr("Increase or decrease spot size by N."); } - print() << " quit " << Main::tr("Quit the running instance."); + print() << " settings=[show|hide] " << Main::tr("Show/hide preferences dialog."); + if (fullHelp) { + print() << " preset=NAME " << Main::tr("Set a preset."); + } + print() << " quit " << Main::tr("Quit the running instance."); // Early return if the user not explicitly requested the full help if (!fullHelp) { return; } From e95c0764197f2d1c06f962a67af7a79529ef292d Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 17 Oct 2023 10:09:59 +0200 Subject: [PATCH 109/110] fix: build-type for source-archives --- cmake/modules/GitVersion.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmake/modules/GitVersion.cmake b/cmake/modules/GitVersion.cmake index 61311e87..d4bc89fe 100644 --- a/cmake/modules/GitVersion.cmake +++ b/cmake/modules/GitVersion.cmake @@ -397,6 +397,8 @@ function(add_version_info_custom_prefix target prefix directory) include(ArchiveVersionInfo_${prefix} OPTIONAL RESULT_VARIABLE ARCHIVE_VERSION_PRESENT) if(ARCHIVE_VERSION_PRESENT AND ${prefix}_VERSION_SUCCESS) message(STATUS "Info: Version information from archive file.") + set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}") + set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}" PARENT_SCOPE) else() get_version_info(${prefix} "${directory}") if("${${prefix}_VERSION_FULLHASH}" STREQUAL "unknown" From b4ae0b56fe191bde63010aaf3074c2860f70ac2c Mon Sep 17 00:00:00 2001 From: Jahn Date: Tue, 17 Oct 2023 10:11:41 +0200 Subject: [PATCH 110/110] ci: bump vor release 0.10 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2eb00177..6d1691ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,8 +176,8 @@ set_target_properties(projecteur PROPERTIES VERSION_MAJOR 0 VERSION_MINOR 10 VERSION_PATCH 0 - VERSION_TYPE develop - VERSION_DISTANCE_OFFSET 200 + VERSION_TYPE release + VERSION_DISTANCE_OFFSET 0 ) add_version_info(projecteur "${CMAKE_CURRENT_SOURCE_DIR}")