diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml new file mode 100644 index 0000000..18b8028 --- /dev/null +++ b/.github/workflows/build-ubuntu.yml @@ -0,0 +1,90 @@ +name: Ubuntu + +on: + push: + pull_request: + branches: + - main + +jobs: + build-ubuntu: + + strategy: + matrix: + os: [ubuntu-22.04, ubuntu-20.04] + + runs-on: ${{ matrix.os }} + + steps: + + - name: Set Variables + run: | + if [[ '${{ matrix.os }}' == 'ubuntu-22.04' ]]; then + echo "CC_PATH=/usr/bin/gcc-11" >> "$GITHUB_ENV" + echo "CXX_PATH=/usr/bin/g++-11" >> "$GITHUB_ENV" + elif [[ '${{ matrix.os }}' == 'ubuntu-20.04' ]]; then + echo "CC_PATH=/usr/bin/gcc-9" >> "$GITHUB_ENV" + echo "CXX_PATH=/usr/bin/g++-9" >> "$GITHUB_ENV" + else + echo "CC_PATH=/usr/bin/gcc" >> "$GITHUB_ENV" + echo "CXX_PATH=/usr/bin/g++" >> "$GITHUB_ENV" + fi + + - name: Install Dependencies + run: | + sudo add-apt-repository ppa:ecal/ecal-latest + sudo apt-get update + sudo apt-get install ecal qtmultimedia5-dev libqt5multimedia5-plugins qtwayland5 libprotoc-dev protobuf-compiler libhdf5-dev + + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + fetch-depth: 0 + + - name: CMake + run: | + export CC=${{ env.CC_PATH }} + export CXX=${{ env.CXX_PATH }} + + mkdir "${{ runner.workspace }}/_build" + cd "${{ runner.workspace }}/_build" + cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_LIBDIR=lib/x86_64-linux-gnu \ + "${{ github.workspace }}" + + shell: bash + + - name: Build Release + run: cmake --build . --config Release + working-directory: ${{ runner.workspace }}/_build + + - name: Extract eCAL Version Number + run: | + ECAL_VERSION=$(dpkg -s ecal | grep '^Version:' | sed 's@^[^0-9]*\([0-9.]\+\).*@\1@' | cut -d"." -f-2) + echo "ecal_version=${ECAL_VERSION}" >> "$GITHUB_ENV" + + - name: Read Project Name from CMakeCache + run: | + CMAKE_PROJECT_NAME_STRING=$(cat "${{runner.workspace}}/_build/CMakeCache.txt" | grep "^CMAKE_PROJECT_NAME:") + arr=(${CMAKE_PROJECT_NAME_STRING//=/ }) + CMAKE_PROJECT_NAME=${arr[1]} + echo "cmake_project_name=$CMAKE_PROJECT_NAME" >> "$GITHUB_ENV" + shell: bash + + - name: Pack + run: cpack -G DEB + working-directory: ${{ runner.workspace }}/_build + + - name: Rename .deb installer + run: | + mv *.deb "${{env.cmake_project_name}}-${{ matrix.os }}-ecal-${{env.ecal_version}}.deb" + shell: bash + working-directory: ${{runner.workspace}}/_build/_deploy/ + + + - name: Upload Debian + uses: actions/upload-artifact@v3 + with: + name: ${{env.cmake_project_name}}-${{ matrix.os }}-ecal-${{env.ecal_version}} + path: ${{ runner.workspace }}/_build/_deploy/*.deb \ No newline at end of file diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 0000000..d9d8cc9 --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,67 @@ +name: Windows + +on: + push: + branches: + - main + +jobs: + build-windows: + runs-on: windows-2019 + + steps: + - name: Download eCAL + uses: robinraju/release-downloader@v1.6 + with: + repository: "eclipse-ecal/ecal" + latest: true + fileName: "ecal_*-win64.exe" + + - name: Extract eCAL Version + run: | + echo "ECAL_VERSION=$(ls | sed 's@^[^0-9]*\([0-9.]\+\).*@\1@' | cut -d"." -f-2)" >> "$GITHUB_ENV" + shell: bash + + - name: Install eCAL + run: Start-Process -Wait -FilePath ".\ecal_*-win64.exe" -ArgumentList "/SILENT /ALLUSERS /SUPPRESSMSGBOXES /NORESTART /TYPE=full" + + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + fetch-depth: 0 + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: '5.15.2' + target: 'desktop' + arch: 'win64_msvc2015_64' + + - name: CMake + run: | + mkdir "${{ runner.workspace }}\_build" + cd "${{ runner.workspace }}\_build" + cmake -G "Visual Studio 16 2019" -Tv140 -Ax64 ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON ^ + -DCMAKE_PREFIX_PATH="C:/eCAL/lib/cmake/;C:/eCAL/cmake" ^ + -DCMAKE_MODULE_PATH="C:/eCAL/cmake" ^ + -DCMAKE_INSTALL_PREFIX="${{ runner.workspace }}\_deploy" ^ + %GITHUB_WORKSPACE% + shell: cmd + + - name: Build + run: cmake --build . --config Release + working-directory: ${{ runner.workspace }}\_build + + - name: Install + run: | + cmake --build . --config Release --target install + working-directory: ${{ runner.workspace }}\_build + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: ecal-camera-samples-windows-ecal-${{env.ECAL_VERSION}} + path: ${{ runner.workspace }}\_deploy\bin \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dc47ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# CMake build folders +/_build +/build + +# Visual Studio 2015/2017 cache/options directory +.vs/ + +# Visual Studio code +*.vscode + +# QtCreator temporary files +CMakeLists.txt.user + +# Clion +.idea +cmake-build-* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cfe868b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,142 @@ +# ========================= LICENSE ================================= +# +# Copyright (C) 2022 Continental Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ========================= LICENSE ================================= + +cmake_minimum_required(VERSION 3.4) +project(ecal-camera-samples) + +set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_NAME}) + +set(CPACK_PACKAGE_VERSION_MAJOR 1) +set(CPACK_PACKAGE_VERSION_MINOR 0) +set(CPACK_PACKAGE_VERSION_PATCH 0) +set(CPACK_PACKAGE_INSTALL_DIRECTORY ${PROJECT_NAME}) +set(CPACK_PACKAGE_CONTACT "mirza.canovic@gmail.com") +set(CPACK_OUTPUT_FILE_PREFIX _deploy) + +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) + +include(CPack) + +add_definitions(-DQWT_DLL) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(UNIX) + set(CMAKE_LINK_WHAT_YOU_USE ON) +endif(UNIX) +# Set CMake policy behavior (alias targets) +cmake_policy(SET CMP0028 NEW) +cmake_policy(SET CMP0048 NEW) +cmake_policy(SET CMP0057 NEW) +cmake_policy(SET CMP0054 NEW) +cmake_policy(SET CMP0080 NEW) + +# -------------------------------------------------------- +# set ECAL_MON_PLUGIN_DIR install dir +# -------------------------------------------------------- +include(GNUInstallDirs) +if(WIN32) + set(ECAL_MON_PLUGIN_DIR ecalmon_plugins) +else() + set(ECAL_MON_PLUGIN_DIR ecal/plugins/mon) +endif() + +set(eCAL_install_lib_dir ${CMAKE_INSTALL_LIBDIR}) +set(eCAL_install_bin_dir ${CMAKE_INSTALL_BINDIR}) + +if (WIN32) + if( CMAKE_SIZEOF_VOID_P EQUAL 8 ) + set(PLATFORM_NAME "x64") + else() + set(PLATFORM_NAME "win32") + endif() + set(VS_PLATFORM "v140_${PLATFORM_NAME}") +endif() + +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake_utils/mon_functions.cmake) + +# -------------------------------------------------------- +# command line build options +# use it that way cmake .. -DHAS_OPENCV=ON +# -------------------------------------------------------- +include(CMakeDependentOption) + +# -------------------------------------------------------- +# detect qt library +# -------------------------------------------------------- +if(WIN32) + if(NOT DEFINED ENV{QT5_ROOT_DIRECTORY}) + # If the QT5_ROOT_DIRECTORY Variable is not set, there is still a chance that + # the user set the CMAKE_PREFIX_PATH himself. Thus we try to find + # Qt5 Core just to see if that works. If we can find Qt, we assume + # that the user knows what he is doing. + find_package(Qt5 COMPONENTS Core) + if (NOT Qt5_FOUND) + message(FATAL_ERROR "QT5_ROOT_DIRECTORY is not defined. Please create an \ + environment variable QT5_ROOT_DIRECTORY pointing to your QT5 installation \ + (the directory that contains the msvc2015 or msvc2015_64 \ + directory)") + endif() + else() + if(NOT CMAKE_CL_64) + # Add 32bit Qt5 + list(APPEND CMAKE_PREFIX_PATH $ENV{QT5_ROOT_DIRECTORY}/msvc2015/) + message(STATUS "QT 32 Bit") + message(STATUS "CMAKE_PREFIX_PATH appended: $ENV{QT5_ROOT_DIRECTORY}/msvc2015/") + else () + # Add 64bit Qt5 + list(APPEND CMAKE_PREFIX_PATH $ENV{QT5_ROOT_DIRECTORY}/msvc2015_64/) + message(STATUS "QT 64 Bit") + message(STATUS "CMAKE_PREFIX_PATH appended: $ENV{QT5_ROOT_DIRECTORY}/msvc2015_64/") + endif() + endif() +endif(WIN32) + +if(WIN32) + include(${CMAKE_CURRENT_LIST_DIR}/cmake_utils/qt/qt_windeployqt.cmake) +endif() + + +if(POLICY CMP0071) + cmake_policy(SET CMP0071 NEW) +endif() + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC OFF) # Reason for being turned off: AutoUIC will prevent VS from detecting changes in .ui files +set(CMAKE_AUTORCC OFF) # Reason for being turned off: AutoRCC will create an entirely new project in VS which clutters the solution appearance. Additionally, we cannot assign a source group to the generated .cpp files which will clutter the project. +set(CMAKE_INCLUDE_CURRENT_DIR ON) +find_package(eCAL COMPONENTS core pb mon_plugin_lib REQUIRED) +find_package(Qt5 COMPONENTS Core Widgets REQUIRED) +find_package(Protobuf REQUIRED) + +create_targets_protobuf() +set(THREADS_PREFER_PTHREAD_FLAG ON) + +if(UNIX) + message(STATUS "GCC detected - Adding flags") + set(CMAKE_CXX_STANDARD 14) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + add_definitions(-fno-pie -fno-PIE -fPIC) + if(NOT CMAKE_VERSION VERSION_LESS 3.10 AND NOT OpenGL_GL_PREFERENCE) + set(OpenGL_GL_PREFERENCE GLVND) + endif() +endif() + + +add_subdirectory(ecal_camera_snd) +add_subdirectory(camera_receiver_mon_plugin) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca5ccae --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# ecal-camera-samples + +The ecal-camera-samples is a set of projects around sending and receiving image data with [eCAL](https://eclipse-ecal.github.io/ecal/). + +After installation, you have 2 components available: + +1. A **sender application** `ecal_camera_snd` that publishes the data of a webcam device to an eCAL topic +2. A **monitor plugin** that can display the image data in the eCAL Monitor + +# How to install prebuilt binaries + +1. [Install eCAL](https://eclipse-ecal.github.io/ecal/getting_started/setup.html) (obviously 😉) +2. Go to the [release page](https://github.com/eclipse-ecal/ecal-camera-samples/releases) and pick the version matching your OS and eCAL Version + +3. **Windows**: Copy the content of the zip archive to the bin directory of your eCAL Installation, probably at `C:\eCAL\bin\` + + **Ubuntu**: Install the dependencies and the downloaded .deb installer: + + ```bash + sudo apt install ecal libqt5multimedia5-plugins + sudo dpkg -i ecal-camera-samples*.deb + ``` + +# How to build it yourself + +## Windows + +1. Install dependencies: + - [eCAL](https://eclipse-ecal.github.io/ecal/getting_started/setup.html) + - [Qt](https://www.qt.io/download) 5.15.2 `msvc2015_x64` + - [CMake](https://cmake.org/) + +2. Configure with CMake and build the Project + ```bat + mkdir _build + cd _build + cmake .. -DCMAKE_PREFIX_PATH=C:/Qt/5.15.2/msvc2015_64 + + cmake --build . --config Release + ``` + +## Ubuntu + +1. Install dependencies: + + Add the eCAL PPA like described here: https://eclipse-ecal.github.io/ecal/getting_started/setup.html#fa-ubuntu-automatically-install-ecal-from-a-ppa + + ```bash + sudo apt install ecal \ + qtmultimedia5-dev \ + libqt5multimedia5-plugins \ + qtwayland5 \ + protobuf-compiler \ + libprotoc-dev \ + libhdf5-dev \ + cmake + ``` + +2. Configure with CMake and compile + + ```bash + mkdir _build + cd _build + cmake .. -DCMAKE_BUILD_TYPE=Release + + cmake --build . + ``` + +# Usage + +## eCAL Camera Sender + +This application captures the stream from a specified camera and publishes it via eCAL. Optionally, the resolution of the image as well as a maximal framerate can be specified. + +Usage: +``` +ecal_camera_snd [topicName] [cameraName] [OPTIONAL_resolutionWidth] [OPTIONAL_resolutionHeight] [OPTIONAL_maxFps] +``` + +Command Line: +``` +topicName: Name of the eCAL Topic to publish to +cameraName: Path to camera. Call --list-cameras to list + available cameras. +resolutionWidth: Image width (optional) +resolutionHeight: Image height (optional) +maxFps: Maximal framerate (optional) + +--help: Print this help +--list-cameras: List all available cameras +``` + +Example: +```bash +ecal_camera_snd compressed_image_protobuf /dev/video0 640 480 10 +``` + +## eCAL Monitor Plugin + +After running cmake --install on the monitor plugin, you can view the published camera image in the eCAL Monitor GUI in the detailed view under the Camera Receiver tab. + +![eCAL Mon Image Plugin](img/mon_cam_plugin.png) \ No newline at end of file diff --git a/camera_receiver_mon_plugin/CMakeLists.txt b/camera_receiver_mon_plugin/CMakeLists.txt new file mode 100644 index 0000000..f5fe490 --- /dev/null +++ b/camera_receiver_mon_plugin/CMakeLists.txt @@ -0,0 +1,83 @@ +# ========================= LICENSE ================================= +# +# Copyright (C) 2022 Continental Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ========================= LICENSE ================================= + +project(mon_plugin_camera_receiver VERSION 0.1.0) + +find_package(Qt5 COMPONENTS + Core + Widgets +REQUIRED) + +find_package(eCAL REQUIRED) +find_package(Protobuf REQUIRED) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC OFF) # Reason for being turned off: AutoUIC will prevent VS from detecting changes in .ui files +set(CMAKE_AUTORCC OFF) # Reason for being turned off: AutoRCC will create an entirely new project in VS which clutters the solution appearance. Additionally, we cannot assign a source group to the generated .cpp files which will clutter the project. +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(${PROJECT_NAME}_src + src/plugin_widget.cpp + src/plugin.cpp +) + +set(${PROJECT_NAME}_header + src/plugin_widget.h + src/plugin.h +) + +set(${PROJECT_NAME}_ui + src/plugin_widget.ui +) + +qt5_wrap_ui(autogen_ui ${${PROJECT_NAME}_ui}) + +add_mon_plugin(${PROJECT_NAME} + ${${PROJECT_NAME}_src} ${${PROJECT_NAME}_header} ${${PROJECT_NAME}_ui} ${autogen_ui} + src/metadata.json +) + +set(protobuf_files + ${CMAKE_SOURCE_DIR}/proto_messages/compressed_image.proto +) +PROTOBUF_TARGET_CPP(${PROJECT_NAME} ${CMAKE_SOURCE_DIR}/proto_messages/ ${protobuf_files}) + + +create_targets_protobuf() +target_link_libraries(${PROJECT_NAME} Qt5::Widgets eCAL::core protobuf::libprotobuf eCAL::mon_plugin_lib) + +if(MSVC) + set_target_properties(${PROJECT_NAME} PROPERTIES COMPILE_FLAGS "/wd4127 /wd4714") +ENDIF(MSVC) + +target_include_directories(${PROJECT_NAME} PRIVATE src) +target_include_directories(${PROJECT_NAME} PRIVATE $) + +install_mon_plugin(${PROJECT_NAME}) + +if (TARGET mon) +add_dependencies(mon ${PROJECT_NAME}) +endif() + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY CMAKE_BINARY_DIR/lib) +set(ECAL_MON_PLUGIN_DIR ecalmon_plugins) + +set_target_properties(${PROJECT_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$/${ECAL_MON_PLUGIN_DIR} + FOLDER app/mon/plugins +) diff --git a/camera_receiver_mon_plugin/src/metadata.json b/camera_receiver_mon_plugin/src/metadata.json new file mode 100644 index 0000000..b9cbe4c --- /dev/null +++ b/camera_receiver_mon_plugin/src/metadata.json @@ -0,0 +1,16 @@ +{ + "Version": "v0.1.0", + "Author": "Emil-Adrian Bic", + "Name": "Camera Receiver", + + "Priority": 1, + + "SupportedTopics": + [ + { + "Format": "proto" + } + ], + + "OptionalTopics": [] +} \ No newline at end of file diff --git a/camera_receiver_mon_plugin/src/plugin.cpp b/camera_receiver_mon_plugin/src/plugin.cpp new file mode 100644 index 0000000..df845c4 --- /dev/null +++ b/camera_receiver_mon_plugin/src/plugin.cpp @@ -0,0 +1,28 @@ +/* ========================= LICENSE ================================= +* +* Copyright (C) 2022 Continental Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ========================= LICENSE ================================= +*/ + +#include "plugin_widget.h" +#include "plugin.h" + +using namespace eCAL::mon; + +PluginWidgetInterface* Plugin::create(const QString& topic_name, const QString& topic_type, QWidget* parent) +{ + return new PluginWidget(topic_name, topic_type, parent); +} \ No newline at end of file diff --git a/camera_receiver_mon_plugin/src/plugin.h b/camera_receiver_mon_plugin/src/plugin.h new file mode 100644 index 0000000..cb1fbc4 --- /dev/null +++ b/camera_receiver_mon_plugin/src/plugin.h @@ -0,0 +1,31 @@ +/* ========================= LICENSE ================================= +* +* Copyright (C) 2022 Continental Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ========================= LICENSE ================================= +*/ + +#pragma once + +#include + +class Plugin : public QObject, public eCAL::mon::PluginInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "de.conti.ecal.monitor.plugin.camera-receiverv/v0.1.0" FILE "metadata.json") + Q_INTERFACES(eCAL::mon::PluginInterface) +public: + virtual eCAL::mon::PluginWidgetInterface* create(const QString& topic_name, const QString& topic_type, QWidget* parent = Q_NULLPTR); +}; \ No newline at end of file diff --git a/camera_receiver_mon_plugin/src/plugin_widget.cpp b/camera_receiver_mon_plugin/src/plugin_widget.cpp new file mode 100644 index 0000000..de15d19 --- /dev/null +++ b/camera_receiver_mon_plugin/src/plugin_widget.cpp @@ -0,0 +1,167 @@ +/* ========================= LICENSE ================================= +* +* Copyright (C) 2022 Continental Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ========================= LICENSE ================================= +*/ + +#include "plugin_widget.h" + +#include +#include +#include + +PluginWidget::PluginWidget(const QString& topic_name, const QString& topic_type, QWidget* parent): + QWidget(parent), + compressed_image_subscriber_(topic_name.toStdString()), + last_received_photo_(nullptr), + last_message_publish_timestamp_(eCAL::Time::ecal_clock::time_point(eCAL::Time::ecal_clock::duration(-1))), + topic_name_(topic_name), + topic_type_(topic_type), + new_msg_available_(false), + received_message_counter_(0) +{ + ui_.setupUi(this); + + // Timestamp warning + int label_height = ui_.publish_timestamp_warning_label->sizeHint().height(); + QPixmap warning_icon = QPixmap(":/ecalicons/WARNING").scaled(label_height, label_height, Qt::AspectRatioMode::KeepAspectRatio, Qt::TransformationMode::SmoothTransformation); + ui_.publish_timestamp_warning_label->setPixmap(warning_icon); + ui_.publish_timestamp_warning_label->setVisible(false); + + // Add eCAL Callbacks + compressed_image_subscriber_.AddReceiveCallback(std::bind(&PluginWidget::photoReceivedMessageCallback, this, std::placeholders::_2, std::placeholders::_3)); + +} + +PluginWidget::~PluginWidget() +{ +#ifndef NDEBUG + // call the function via its class becase it's a virtual function that is called in constructor/destructor,- + // where the vtable is not created yet or it's destructed. + qDebug().nospace() << "[" << PluginWidget::metaObject()->className() << "]: Deleting Widget for topic " << topic_name_; +#endif // NDEBUG + + compressed_image_subscriber_.RemReceiveCallback(); + + { + std::lock_guard lock(proto_message_mutex_); + delete last_received_photo_; + } +} + +void PluginWidget::updatePublishTimeLabel() +{ + eCAL::Time::ecal_clock::time_point publish_time = last_message_publish_timestamp_; + eCAL::Time::ecal_clock::time_point receive_time = eCAL::Time::ecal_clock::now(); + + if (publish_time < eCAL::Time::ecal_clock::time_point(eCAL::Time::ecal_clock::duration(0))) + return; + + auto diff = receive_time - publish_time; + + if ((diff > std::chrono::milliseconds(100)) + || (diff < std::chrono::milliseconds(-100))) + { + ui_.publish_timestamp_warning_label->setVisible(true); + QString diff_string = QString::number(std::chrono::duration_cast>(diff).count(), 'f', 6); + ui_.publish_timestamp_warning_label->setToolTip(tr("The publisher is not synchronized, properly.\nCurrent time difference: ") + diff_string + " s"); + } + else + { + ui_.publish_timestamp_warning_label->setVisible(false); + } + + QString time_string; + + //if (parse_time_) + //{ + // QDateTime q_ecal_time = QDateTime::fromMSecsSinceEpoch(std::chrono::duration_cast(publish_time.time_since_epoch()).count()).toUTC(); + // time_string = q_ecal_time.toString("yyyy-MM-dd HH:mm:ss.zzz"); + //} + //else + { + double seconds_since_epoch = std::chrono::duration_cast>(publish_time.time_since_epoch()).count(); + time_string = QString::number(seconds_since_epoch, 'f', 6) + " s"; + } + + ui_.publish_timestamp_label->setText(time_string); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Content Update //// +//////////////////////////////////////////////////////////////////////////////// + +// eCAL Callback +void PluginWidget::photoReceivedMessageCallback(const foxglove::CompressedImage& compressed_image_msg, long long send_time_usecs) +{ + { + // Lock the mutex + std::lock_guard lock(proto_message_mutex_); + // Delete the old message + delete last_received_photo_; + + // Create a copy of the new message as member variable. We cannot use a reference here, as this may cause a deadlock with the GUI thread + last_received_photo_ = compressed_image_msg.New(); + last_received_photo_->CopyFrom(compressed_image_msg); + + eCAL::Logging::Log(eCAL_Logging_eLogLevel::log_level_info, "Received " + compressed_image_msg.format() + " photo of size " + + std::to_string(last_received_photo_->data().length())); + + last_message_publish_timestamp_ = eCAL::Time::ecal_clock::time_point(std::chrono::duration_cast(std::chrono::microseconds(send_time_usecs))); + + new_msg_available_ = true; + received_message_counter_++; + } +} + +// Actual Content Update +void PluginWidget::updateContent() +{ + auto photo_data = last_received_photo_->data(); + QByteArray byte_array(photo_data.c_str(), photo_data.length()); + QPixmap pixmap; + pixmap.loadFromData(byte_array, "JPG"); + + ui_.photo_label->setPixmap(pixmap.scaled(ui_.photo_label->size(), Qt::AspectRatioMode::KeepAspectRatio, Qt::TransformationMode::SmoothTransformation)); + + new_msg_available_ = false; +} + +void PluginWidget::onUpdate() +{ + if (new_msg_available_) + { + updateContent(); + updatePublishTimeLabel(); + ui_.received_message_counter_label->setText(QString::number(received_message_counter_)); + } +} + +void PluginWidget::onResume() +{ + // Add eCAL Callbacks + compressed_image_subscriber_.AddReceiveCallback(std::bind(&PluginWidget::photoReceivedMessageCallback, this, std::placeholders::_2, std::placeholders::_3)); +} + +void PluginWidget::onPause() +{ + compressed_image_subscriber_.RemReceiveCallback(); +} + +QWidget* PluginWidget::getWidget() +{ + return this; +} diff --git a/camera_receiver_mon_plugin/src/plugin_widget.h b/camera_receiver_mon_plugin/src/plugin_widget.h new file mode 100644 index 0000000..4f88402 --- /dev/null +++ b/camera_receiver_mon_plugin/src/plugin_widget.h @@ -0,0 +1,70 @@ +/* ========================= LICENSE ================================= +* +* Copyright (C) 2022 Continental Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ========================= LICENSE ================================= +*/ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include + +#include "ui_plugin_widget.h" + +#include "compressed_image.pb.h" + +class PluginWidget : public QWidget, public eCAL::mon::PluginWidgetInterface +{ + Q_OBJECT +public: + PluginWidget(const QString& topic_name, const QString& topic_type, QWidget* parent = Q_NULLPTR); + virtual ~PluginWidget(); + + virtual QWidget* getWidget(); + +public slots: + virtual void onUpdate(); + + virtual void onResume(); + virtual void onPause(); + +protected: + Ui::PluginWidget ui_; + +private slots: + void updateContent(); + +private: + eCAL::protobuf::CSubscriber compressed_image_subscriber_; + + std::mutex proto_message_mutex_; + foxglove::CompressedImage* last_received_photo_; + eCAL::Time::ecal_clock::time_point last_message_publish_timestamp_; + + QString topic_name_, topic_type_; + bool new_msg_available_; + int received_message_counter_; + + void photoReceivedMessageCallback(const foxglove::CompressedImage& message, long long send_time_usecs); + void updatePublishTimeLabel(); +}; diff --git a/camera_receiver_mon_plugin/src/plugin_widget.ui b/camera_receiver_mon_plugin/src/plugin_widget.ui new file mode 100644 index 0000000..4b85b78 --- /dev/null +++ b/camera_receiver_mon_plugin/src/plugin_widget.ui @@ -0,0 +1,155 @@ + + + PluginWidget + + + + 0 + 0 + 450 + 550 + + + + GroupWidget + + + + + + + + + 0 + 0 + + + + + 1920 + 1080 + + + + No photo received yet + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + 10 + 0 + + + + Publish timestamp: + + + + + + + + 0 + 0 + + + + WARNING + + + + + + + + 0 + 0 + + + + + 10 + 0 + + + + - + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 10 + 0 + + + + Received messages: + + + + + + + + 0 + 0 + + + + + 10 + 0 + + + + 0 + + + + + + + + + + + + diff --git a/cmake_utils/cmake_functions.cmake b/cmake_utils/cmake_functions.cmake new file mode 100644 index 0000000..cccc7b3 --- /dev/null +++ b/cmake_utils/cmake_functions.cmake @@ -0,0 +1,27 @@ +set (file_list_include) + +if(WIN32) + list(APPEND file_list_include + qt/qt_msvc_path.cmake + qt/qt_windeployqt.cmake + ) +endif() + +set(file_list_no_include) + +if(WIN32) + list(APPEND file_list_no_include + qt/qt_windeployqt_threadsafe_cmake.bat.in + ) +endif() + +# Set list of all files to be installed by CMake Script. +set(file_list + ${file_list_include} + ${file_list_no_include} +) + +# Include all files which need to be included +foreach(f ${file_list_include}) + include(${CMAKE_CURRENT_LIST_DIR}/${f}) +endforeach() \ No newline at end of file diff --git a/cmake_utils/mon_functions.cmake b/cmake_utils/mon_functions.cmake new file mode 100644 index 0000000..35ac35a --- /dev/null +++ b/cmake_utils/mon_functions.cmake @@ -0,0 +1,39 @@ +# ========================= LICENSE ================================= +# +# Copyright (C) 2022 Continental Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ========================= LICENSE ================================= + +function(add_mon_plugin TARGET_NAME) + + add_library(${TARGET_NAME} SHARED ${ARGN}) + target_link_libraries(${TARGET_NAME} protobuf::libprotobuf eCAL::core eCAL::hdf5 eCAL::mon_plugin_lib eCAL::proto) + target_include_directories(${TARGET_NAME} PRIVATE src) + if(MSVC) + set_target_properties(${TARGET_NAME} PROPERTIES COMPILE_FLAGS "/wd4125 /wd4100 /wd4251 /wd4275 /wd4456 /wd4505 /wd4099") + set_target_properties(${TARGET_NAME} PROPERTIES OUTPUT_NAME ${TARGET_NAME}_mon_plugin) + else(MSVC) + set_target_properties(${TARGET_NAME} PROPERTIES + OUTPUT_NAME ${TARGET_NAME}_mon_plugin) + endif() +endfunction() + +function(install_mon_plugin TARGET_NAME) + install(TARGETS ${TARGET_NAME} + RUNTIME DESTINATION "${eCAL_install_bin_dir}/${ECAL_MON_PLUGIN_DIR}" COMPONENT app + LIBRARY DESTINATION $,${eCAL_install_bin_dir}/${ECAL_MON_PLUGIN_DIR},${eCAL_install_lib_dir}/${ECAL_MON_PLUGIN_DIR}> COMPONENT app + ARCHIVE DESTINATION lib COMPONENT app + ) +endfunction() \ No newline at end of file diff --git a/cmake_utils/qt/qt_msvc_path.cmake b/cmake_utils/qt/qt_msvc_path.cmake new file mode 100644 index 0000000..68495d6 --- /dev/null +++ b/cmake_utils/qt/qt_msvc_path.cmake @@ -0,0 +1,119 @@ +# ========================= LICENSE ================================= +# +# Copyright (C) 2016 - 2019 Continental Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ========================= LICENSE ================================= + +macro(autodetect_qt5_msvc_dir) + SET(QT_MISSING True) + # msvc only; mingw will need different logic + IF(MSVC) + message(STATUS "Trying to auto-detect best Qt5 Version") + # look for user-registry pointing to qtcreator + GET_FILENAME_COMPONENT(QTCREATOR_BIN [HKEY_CURRENT_USER\\Software\\Classes\\Applications\\QtProject.QtCreator.pro\\shell\\Open\\Command] DIRECTORY) + # get root path so we can search for 5.3, 5.4, 5.5, etc + STRING(REPLACE "/Tools" ";" QT_BIN "${QTCREATOR_BIN}") + + LIST(GET QT_BIN 0 QT_INSTALL_DIRECTORY) + + # Collect all Qt5 directories + FILE(GLOB QT_VERSION_DIRECTORIES "${QT_INSTALL_DIRECTORY}/5.*") + + # Compute the MSVC Version as year (e.g. 2015, 2017, 2019...) and aim for upwards compatibility + if (MSVC_VERSION LESS 1900) + message(FATAL_ERROR "ERROR: MSVC_VERSION is $MSVC_VERSION, which indicates Visual Studio < 2015. Only Visual Studio 2015 and up are supported.") + endif() + + if (MSVC_VERSION LESS_EQUAL "1909") + set(MSVC_YEAR "2015") + elseif (MSVC_VERSION LESS_EQUAL "1919") + set(MSVC_YEAR "2017") + elseif (MSVC_VERSION LESS_EQUAL "1929") + set(MSVC_YEAR "2019") + elseif (MSVC_VERSION LESS_EQUAL "1939") + set(MSVC_YEAR "2022") + else () # Assume each 2 years a new Visual Studio Version with a range of 10 MSVC_VERSIONs + MATH(EXPR MSVC_YEAR "2022 + ((${MSVC_VERSION} - 1930) / 10) * 2") + endif () + + message(STATUS "Detected Visual Studio ${MSVC_YEAR} (from MSVC_VERSION ${MSVC_VERSION})") + + # Iterate over all Qt Versions and pick the best + set(BEST_QT_DIRECTORY "") + set(BEST_QT_MSVC_YEAR 0) + set(BEST_QT_MAJOR 0) + set(BEST_QT_MINOR 0) + set(BEST_QT_PATCH 0) + + foreach (QT_DIRECTORY ${QT_VERSION_DIRECTORIES}) + # Get last component of path, which is the qt version + get_filename_component(QT_VERSION_STRING "${QT_DIRECTORY}" NAME) + + # Split the string, to get the major, minor and patch + STRING(REPLACE "." ";" QT_VERSION_COMPONENT_LIST "${QT_VERSION_STRING}") + + LIST(GET QT_VERSION_COMPONENT_LIST 0 QT_MAJOR) + LIST(GET QT_VERSION_COMPONENT_LIST 1 QT_MINOR) + LIST(GET QT_VERSION_COMPONENT_LIST 2 QT_PATCH) + + # Check if there is an installation we can use + if (CMAKE_CL_64) + FILE(GLOB QT_MSVC_DIRECTORIES "${QT_DIRECTORY}/msvc20[0-9][0-9]_64") + else () + FILE(GLOB QT_MSVC_DIRECTORIES "${QT_DIRECTORY}/msvc20[0-9][0-9]") + endif() + + # Iterate over all msvc directories and check for the best matching one + foreach (QT_MSVC_DIR ${QT_MSVC_DIRECTORIES}) + STRING(REPLACE "//" "/" QT_MSVC_DIR "${QT_MSVC_DIR}") + get_filename_component(QT_MSVC_DIR_NAME "${QT_MSVC_DIR}" NAME) + + STRING(REPLACE "msvc" "" QT_MSVC_YEAR "${QT_MSVC_DIR_NAME}") + if (CMAKE_CL_64) + STRING(REPLACE "_64" "" QT_MSVC_YEAR "${QT_MSVC_YEAR}") + endif() + + if (("${QT_MSVC_YEAR}" LESS_EQUAL "${MSVC_YEAR}") AND (${QT_MSVC_YEAR} GREATER_EQUAL "2015")) + # At this point we have found a version that should be usable. Let's check if it is better than the last one we found! + message(STATUS "Found Qt5 candidate at ${QT_MSVC_DIR}") + if ( ("${QT_MAJOR}" GREATER "${BEST_QT_MAJOR}") OR + (("${QT_MAJOR}" EQUAL "${BEST_QT_MAJOR}") AND ("${QT_MINOR}" GREATER "${BEST_QT_MINOR}")) OR + (("${QT_MAJOR}" EQUAL "${BEST_QT_MAJOR}") AND ("${QT_MINOR}" EQUAL "${BEST_QT_MINOR}") AND ("${QT_PATCH}" GREATER "${BEST_QT_PATCH}")) OR + (("${QT_MAJOR}" EQUAL "${BEST_QT_MAJOR}") AND ("${QT_MINOR}" EQUAL "${BEST_QT_MINOR}") AND ("${QT_PATCH}" EQUAL "${BEST_QT_PATCH}") AND ("${QT_MSVC_YEAR}" GREATER "${BEST_QT_MSVC_YEAR}")) + ) + + set(BEST_QT_DIRECTORY "${QT_MSVC_DIR}") + set(BEST_QT_MSVC_YEAR "${QT_MSVC_YEAR}") + set(BEST_QT_MAJOR "${QT_MAJOR}") + set(BEST_QT_MINOR "${QT_MINOR}") + set(BEST_QT_PATCH "${QT_PATCH}") + + endif() + + endif() + + endforeach() + endforeach() + + if ("${BEST_QT_DIRECTORY}" STREQUAL "") + message(FATAL_ERROR "ERROR: Unable to find any usable Qt5 installation. Please install Qt5 or manually set the CMAKE_PREFIX_PATH.") + endif() + + message(STATUS "Using Qt ${BEST_QT_MAJOR}.${BEST_QT_MINOR}.${BEST_QT_PATCH} MSVC ${BEST_QT_MSVC_YEAR} from ${BEST_QT_DIRECTORY}") + SET(Qt5_DIR "${BEST_QT_DIRECTORY}/lib/cmake/Qt5/") + SET(Qt5Test_DIR "${BEST_QT_DIRECTORY}/lib/cmake/Qt5Test") + + endif() +endmacro() \ No newline at end of file diff --git a/cmake_utils/qt/qt_windeployqt.cmake b/cmake_utils/qt/qt_windeployqt.cmake new file mode 100644 index 0000000..941b219 --- /dev/null +++ b/cmake_utils/qt/qt_windeployqt.cmake @@ -0,0 +1,58 @@ +# ========================= LICENSE ================================= +# +# Copyright (C) 2016 - 2019 Continental Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ========================= LICENSE ================================= + + +set (qt_windeployqt_cmake_path ${CMAKE_CURRENT_LIST_DIR}) + +# Create convenient function to run windeployqt +function(qt_add_windeployqt_postbuild arguments) + # Declare windeployqt as executable target + if(Qt5_FOUND AND WIN32 AND TARGET Qt5::qmake AND NOT TARGET Qt5::windeployqt) + get_target_property(_qt5_qmake_location + Qt5::qmake IMPORTED_LOCATION + ) + + execute_process( + COMMAND "${_qt5_qmake_location}" -query QT_INSTALL_PREFIX + RESULT_VARIABLE return_code + OUTPUT_VARIABLE qt5_install_prefix + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + set(imported_location "${qt5_install_prefix}/bin/windeployqt.exe") + + if(EXISTS ${imported_location}) + add_executable(Qt5::windeployqt IMPORTED) + + set_target_properties(Qt5::windeployqt PROPERTIES + IMPORTED_LOCATION ${imported_location} + ) + endif() + endif() + + if(TARGET Qt5::windeployqt) + configure_file(${qt_windeployqt_cmake_path}/qt_windeployqt_threadsafe_cmake.bat.in qt_windeployqt_threadsafe_cmake.bat + NEWLINE_STYLE DOS + ) + + add_custom_command(TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND call ${CMAKE_CURRENT_BINARY_DIR}/qt_windeployqt_threadsafe_cmake.bat ${ARGV} + ) + endif() +endfunction(qt_add_windeployqt_postbuild) \ No newline at end of file diff --git a/cmake_utils/qt/qt_windeployqt_threadsafe_cmake.bat.in b/cmake_utils/qt/qt_windeployqt_threadsafe_cmake.bat.in new file mode 100644 index 0000000..ae8de94 --- /dev/null +++ b/cmake_utils/qt/qt_windeployqt_threadsafe_cmake.bat.in @@ -0,0 +1,18 @@ +@echo off & setlocal + +set need_to_wait=0 +set "PATH=${qt5_install_prefix}/bin/;%PATH%" + +:LockFile +( + if %need_to_wait%==1 ( + timeout /NOBREAK /t 1 + ) + set need_to_wait=1 + + 9>%TMP%\.windeployqt-lock ( + windeployqt.exe %* || exit /b %errorlevel% + ) +) ||goto :LockFile + +endlocal diff --git a/ecal_camera_snd/CMakeLists.txt b/ecal_camera_snd/CMakeLists.txt new file mode 100644 index 0000000..75a2575 --- /dev/null +++ b/ecal_camera_snd/CMakeLists.txt @@ -0,0 +1,93 @@ +# ========================= LICENSE ================================= +# +# Copyright (C) 2022 Continental Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ========================= LICENSE ================================= + +cmake_minimum_required(VERSION 3.0) +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) + +project(ecal_camera_snd) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) + +find_package(eCAL REQUIRED) +find_package(Protobuf REQUIRED) +find_package(Qt5 COMPONENTS Widgets REQUIRED) +find_package(Qt5MultimediaWidgets REQUIRED) + +set(source_files + main.cpp + camera_wrapper.h + camera_wrapper.cpp + camera_wrapper_with_fps_control.h + camera_wrapper_with_fps_control.cpp +) + +set(protobuf_files + ${CMAKE_SOURCE_DIR}/proto_messages/compressed_image.proto +) + +add_executable(${PROJECT_NAME} ${source_files}) + +PROTOBUF_TARGET_CPP(${PROJECT_NAME} ${CMAKE_SOURCE_DIR}/proto_messages/ ${protobuf_files}) + +target_link_libraries(${PROJECT_NAME} + eCAL::core + protobuf::libprotobuf + Qt5::Widgets + Qt5::MultimediaWidgets +) + +if(WIN32) + # Deploy Qt DLLs in the binary folder. This is necessary for starting the application from whithin the IDE without having to copy QtCore.dll, QtWidgets.dll etc. by hand each time + qt_add_windeployqt_postbuild(--no-system-d3d-compiler --libdir ${CMAKE_SOURCE_DIR}/../_deploy/bin --plugindir ${CMAKE_SOURCE_DIR}/../_deploy/bin --no-compiler-runtime --no-opengl-sw --pdb "$") +endif() + +install(TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin COMPONENT app +) + +if(WIN32) +install(CODE + " + set(_file ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Release/ecal_play_gui.exe) + execute_process( + COMMAND \"${CMAKE_COMMAND}\" -E + env PATH=\"${_qt_bin_dir}\" \"${WINDEPLOYQT_EXECUTABLE}\" + --dry-run + --no-compiler-runtime + --no-angle + --no-opengl-sw + --list mapping + \${_file} + OUTPUT_VARIABLE _output + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + separate_arguments(_files WINDOWS_COMMAND \${_output}) + while(_files) + list(GET _files 0 _src) + list(GET _files 1 _dest) + execute_process( + COMMAND \"${CMAKE_COMMAND}\" -E + copy \${_src} \"\${CMAKE_INSTALL_PREFIX}/bin/\${_dest}\" + ) + list(REMOVE_AT _files 0 1) + endwhile() + " +) +endif() diff --git a/ecal_camera_snd/camera_wrapper.cpp b/ecal_camera_snd/camera_wrapper.cpp new file mode 100644 index 0000000..1f0a86a --- /dev/null +++ b/ecal_camera_snd/camera_wrapper.cpp @@ -0,0 +1,155 @@ +/* ========================= LICENSE ================================= + * + * Copyright (C) 2022 Continental Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ========================= LICENSE ================================= + */ + +#include +#include +#include + +#include +#include + +#include "camera_wrapper.h" + +CameraWrapper::CameraWrapper( + eCAL::protobuf::CPublisher &publisher, + std::string &cameraName, uint16_t width, uint16_t height) + : publisher_(publisher), cameraName_(cameraName), width_(width), + height_(height), photosTaken_(0) { + const QList cameras = QCameraInfo::availableCameras(); + if (cameras.isEmpty()) { + std::cerr << "No available camera to use" << std::endl; + return; + } + + for (const QCameraInfo &cameraInfo : cameras) { + if (cameraInfo.deviceName().toStdString() == cameraName_) { + setCamera(cameraInfo); + return; + } + } + + std::cout << "Selected camera not found, setting to default camera." + << std::endl; + setCamera(QCameraInfo::defaultCamera()); +} + +CameraWrapper::~CameraWrapper() {} + +void CameraWrapper::setCamera(const QCameraInfo &cameraInfo) { + camera_.reset(new QCamera(cameraInfo)); + camera_.data()->setCaptureMode(QCamera::CaptureStillImage); + camera_.data()->load(); + + imageCapture_.reset(new QCameraImageCapture(camera_.data())); + imageCapture_.data()->setCaptureDestination( + QCameraImageCapture::CaptureToBuffer); + + if (isGivenResolutionSupported()) { + QImageEncoderSettings imageSettings; + imageSettings.setCodec("image/jpg"); + imageSettings.setResolution(width_, height_); + imageCapture_.data()->setEncodingSettings(imageSettings); + } else { + std::cout << "Given resolution is not supported from the camera, setting " + "default resolution. List of supported resolutions: "; + auto resolutions = imageCapture_.data()->supportedResolutions(); + for (const auto &it : resolutions) { + std::cout << it.width() << "x" << it.height() << ", "; + } + std::cout << std::endl; + } + + // connect + QObject::connect(imageCapture_.data(), &QCameraImageCapture::imageCaptured, + [=](int id, QImage img) { + ++photosTaken_; + QByteArray buf; + QBuffer buffer(&buf); + buffer.open(QIODevice::WriteOnly); + img.save(&buffer, "JPG"); + std::string protoData(buf.constData(), buf.length()); + buffer.close(); + + // create and send protobuf + foxglove::CompressedImage compressedImageProto; + compressedImageProto.set_data(protoData); + compressedImageProto.set_format("jpg"); + publisher_.Send(compressedImageProto); +#ifdef DEBUG + std::cout << "Sent photo number: " << photosTaken_ + << " with size: " + << compressedImageProto.ByteSizeLong() + << std::endl; +#endif + emit photoSentSignal(); + }); + + QObject::connect(imageCapture_.data(), + &QCameraImageCapture::readyForCaptureChanged, + [=](bool state) { + if (state == true) { + capture(); + } + }); + + QObject::connect(this, &CameraWrapper::photoSentSignal, [this]() { + if (isReadyForCapture()) { + capture(); + } + }); + + for (const auto &frameRateRange : + camera_.data()->supportedViewfinderFrameRateRanges()) { + qDebug() << frameRateRange.minimumFrameRate << ", " + << frameRateRange.maximumFrameRate; + } + + camera_.data()->start(); +} + +void CameraWrapper::printAvailableCameras() { + const QList cameras = QCameraInfo::availableCameras(); + if (cameras.isEmpty()) { + std::cout << "No available camera to use" << std::endl; + return; + } + + std::cout << "Available cameras: " << std::endl; + + for (const QCameraInfo &cameraInfo : cameras) { + std::cout << " " << cameraInfo.deviceName().toStdString() << std::endl; + } +} + +void CameraWrapper::capture() { + camera_.data()->searchAndLock(); + imageCapture_.data()->capture(); + camera_.data()->unlock(); +} + +bool CameraWrapper::isGivenResolutionSupported() { + QList resolutions = imageCapture_.data()->supportedResolutions(); + QSize givenResolution(width_, height_); + + return resolutions.contains(givenResolution); +} + +bool CameraWrapper::isReadyForCapture() { + return imageCapture_.data()->isReadyForCapture(); +} diff --git a/ecal_camera_snd/camera_wrapper.h b/ecal_camera_snd/camera_wrapper.h new file mode 100644 index 0000000..8e8f69a --- /dev/null +++ b/ecal_camera_snd/camera_wrapper.h @@ -0,0 +1,64 @@ +/* ========================= LICENSE ================================= +* +* Copyright (C) 2022 Continental Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ========================= LICENSE ================================= +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "compressed_image.pb.h" + +class CameraWrapper : public QObject { + Q_OBJECT +public: + CameraWrapper( + eCAL::protobuf::CPublisher &publisher, + std::string &camera_name, uint16_t width, uint16_t height); + ~CameraWrapper(); + bool isReadyForCapture(); + + void printAvailableCameras(); + +protected: + virtual void capture(); + +private slots: + void setCamera(const QCameraInfo &cameraInfo); + +private: + bool isGivenResolutionSupported(); + std::string supportedResolutionsList(); + + QScopedPointer camera_; + QScopedPointer imageCapture_; + + eCAL::protobuf::CPublisher &publisher_; + + std::string cameraName_; + uint16_t width_; + uint16_t height_; + size_t photosTaken_; + +signals: + void photoSentSignal(); +}; diff --git a/ecal_camera_snd/camera_wrapper_with_fps_control.cpp b/ecal_camera_snd/camera_wrapper_with_fps_control.cpp new file mode 100644 index 0000000..5c7d2f6 --- /dev/null +++ b/ecal_camera_snd/camera_wrapper_with_fps_control.cpp @@ -0,0 +1,50 @@ +/* ========================= LICENSE ================================= +* +* Copyright (C) 2022 Continental Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ========================= LICENSE ================================= +*/ + +#include "camera_wrapper_with_fps_control.h" + +#include +#include + +CameraWrapperWithFpsControl::CameraWrapperWithFpsControl( + eCAL::protobuf::CPublisher &publisher, + std::string &camera_name, uint16_t width, uint16_t height, uint16_t maxFps) + : CameraWrapper(publisher, camera_name, width, height), + frameIntervalInMs(1000 / maxFps), + lastFrameTimestamp(QDateTime::currentMSecsSinceEpoch()) {} + +CameraWrapperWithFpsControl::~CameraWrapperWithFpsControl() {} + +void CameraWrapperWithFpsControl::capture() { + qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); + #ifdef DEBUG + std::cout << "Time: " << currentTime << std::endl; + #endif + // limit the number of photos captured + qint64 sleepingTime = + currentTime - lastFrameTimestamp < frameIntervalInMs + ? frameIntervalInMs - (currentTime - lastFrameTimestamp) + : 0; + + std::this_thread::sleep_for(std::chrono::milliseconds(sleepingTime)); + + CameraWrapper::capture(); + + lastFrameTimestamp = QDateTime::currentMSecsSinceEpoch(); +} diff --git a/ecal_camera_snd/camera_wrapper_with_fps_control.h b/ecal_camera_snd/camera_wrapper_with_fps_control.h new file mode 100644 index 0000000..00b935a --- /dev/null +++ b/ecal_camera_snd/camera_wrapper_with_fps_control.h @@ -0,0 +1,39 @@ +/* ========================= LICENSE ================================= +* +* Copyright (C) 2022 Continental Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +* ========================= LICENSE ================================= +*/ + +#pragma once + +#include "camera_wrapper.h" + +class CameraWrapperWithFpsControl : public CameraWrapper { + Q_OBJECT +public: + CameraWrapperWithFpsControl( + eCAL::protobuf::CPublisher &publisher, + std::string &camera_name, uint16_t width, uint16_t height, + uint16_t maxFps); + + ~CameraWrapperWithFpsControl(); + + void capture(); + +private: + qint64 frameIntervalInMs; + qint64 lastFrameTimestamp; +}; \ No newline at end of file diff --git a/ecal_camera_snd/main.cpp b/ecal_camera_snd/main.cpp new file mode 100644 index 0000000..95591ef --- /dev/null +++ b/ecal_camera_snd/main.cpp @@ -0,0 +1,112 @@ +/* ========================= LICENSE ================================= + * + * Copyright (C) 2022 Continental Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ========================= LICENSE ================================= + */ + +#include +#include + +#include +#include + +#include +#include + +#include "camera_wrapper.h" +#include "camera_wrapper_with_fps_control.h" +#include "compressed_image.pb.h" + +int main(int argc, char **argv) { + constexpr const char* usage_string = "[topicName] " + "[cameraName] [OPTIONAL_resolutionWidth] " + "[OPTIONAL_resolutionHeight] [OPTIONAL_maxFps]"; + + if (argc == 2 && strcmp(argv[1], "--help") == 0) { + std::cout << "eCAL Camera Sender \n\n"; + std::cout << "Usage:\n"; + std::cout << " " << argv[0] << " " << usage_string << "\n\n"; + std::cout << "Example usage: \n" + << " " << argv[0] << "compressed_image_protobuf /dev/video0 640 480 10 \n\n"; + std::cout << "Command Line Arguments: \n\n"; + std::cout << " topicName: Name of the eCAL Topic to publish to\n"; + std::cout << " cameraName: Path to camera. Call --list-cameras to list available cameras. \n"; + std::cout << " resolutionWidth: Image width (optional) \n"; + std::cout << " resolutionHeight: Image height (optional) \n"; + std::cout << " maxFps: Maximal framerate (optional)\n"; + std::cout << std::endl; + std::cout << " --help: Print this help\n"; + std::cout << " --list-cameras: List all available cameras\n"; + + return 0; + } + + std::string cameraName; + std::string topicName; + uint16_t width = 0; + uint16_t height = 0; + uint16_t maxFps = 0; + + std::shared_ptr camera; + + if (argc == 2 && strcmp(argv[1], "--list-cameras") == 0) { + camera->printAvailableCameras(); + return 0; + } + + if (argc < 3) { + std::cerr << "Invalid parameters, usage: " << std::endl << std::endl; + std::cerr << " " << argv[0] << " " << usage_string + << std::endl + << std::endl; + std::cerr << "Get more help with:" << std::endl + << " " << argv[0] << " --help" + << std::endl + << std::endl; + std::cerr << "List available cameras with:" << std::endl + << " " << argv[0] << " --list-cameras" + << std::endl; + return 0; + } else { + if (argc > 4) { + width = std::stoi(argv[3]); + height = std::stoi(argv[4]); + } + + if (argc == 6) { + maxFps = std::stoi(argv[5]); + } + + topicName = argv[1]; + cameraName = argv[2]; + } + + QApplication app(argc, argv); + + // Initialize eCAL and create a protobuf publisher + eCAL::Initialize(argc, argv, "Image Sender"); + eCAL::protobuf::CPublisher publisher(topicName); + + if (maxFps > 0) { + camera = std::make_shared( + publisher, cameraName, width, height, maxFps); + } else { + camera = + std::make_shared(publisher, cameraName, width, height); + } + + return app.exec(); +} diff --git a/img/mon_cam_plugin.png b/img/mon_cam_plugin.png new file mode 100644 index 0000000..79c06d1 Binary files /dev/null and b/img/mon_cam_plugin.png differ diff --git a/proto_messages/compressed_image.proto b/proto_messages/compressed_image.proto new file mode 100644 index 0000000..a30296e --- /dev/null +++ b/proto_messages/compressed_image.proto @@ -0,0 +1,22 @@ +// Generated by https://github.com/foxglove/schemas + +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +package foxglove; + +// A compressed image +message CompressedImage { + // Timestamp of image + google.protobuf.Timestamp timestamp = 1; + + // Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image. + string frame_id = 4; + + // Compressed image data + bytes data = 2; + + // Image format + string format = 3; +}