From e32cbabe83ee91a42dc56e7524baf7b173e03a03 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Sat, 28 Oct 2023 16:57:54 +0200 Subject: [PATCH] Improvements for v0.2.0 - PIMPL - CI - clang-tidy & format - coverage - unit tests --- .clang-format | 19 + .clang-tidy | 11 + .github/workflows/build.yml | 90 +++ .gitignore | 4 +- CHANGELOG.md | 10 + CMakeLists.txt | 63 +- CMakePresets.json | 4 + cmake/compiler.cmake | 25 + cmake/cppcheck.cmake | 14 - cmake/lib.cmake | 36 +- cmake/version.cmake | 27 + conan_profiles/linux_clang | 10 + conan_profiles/{x86_64_Linux => linux_gcc} | 4 + conan_profiles/windows_gcc | 19 + conan_profiles/x86_64_Cross_Windows | 22 - conanfile.py | 36 +- docker/Dockerfile | 18 +- examples/CMakeLists.txt | 11 +- examples/README.md | 3 - examples/bps.cxx | 79 ++- include/pza/core/client.hxx | 78 +++ include/pza/core/core.hxx | 25 + include/pza/core/device.hxx | 72 +++ include/pza/core/interface.hxx | 48 ++ include/pza/core/utils.hxx | 21 + include/pza/interfaces/ammeter.hxx | 21 + include/pza/interfaces/bpc.hxx | 43 ++ include/pza/interfaces/device.hxx | 28 + include/pza/interfaces/meter.hxx | 33 + include/pza/interfaces/platform.hxx | 23 + include/pza/interfaces/voltmeter.hxx | 21 + scripts/build.sh | 96 --- scripts/docker_pull.sh | 2 +- scripts/docker_registry.sh | 6 +- scripts/format.sh | 53 ++ scripts/install_dependencies.sh | 85 --- source/CMakeLists.txt | 54 +- source/core/attribute.cxx | 50 ++ source/core/attribute.hxx | 99 +++ source/core/client.cxx | 580 ++++++++++++++++++ source/core/core.cxx | 11 + source/core/device.cxx | 295 +++++++++ source/core/interface.cxx | 110 ++++ source/core/interface.hxx | 42 ++ source/core/interface_factory.cxx | 41 ++ source/core/interface_factory.hxx | 20 + source/core/mqtt_service.hxx | 24 + source/core/scanner.cxx | 31 + source/core/scanner.hxx | 52 ++ source/core/version.cxx.in | 29 + source/interfaces/ammeter.cxx | 12 + source/interfaces/bpc.cxx | 117 ++++ source/interfaces/device.cxx | 34 + source/interfaces/meter.cxx | 32 + source/interfaces/platform.cxx | 12 + source/interfaces/voltmeter.cxx | 12 + source/pza/core/attribute.cxx | 90 --- source/pza/core/attribute.hxx | 81 --- source/pza/core/client.cxx | 463 -------------- source/pza/core/client.hxx | 88 --- source/pza/core/core.cxx | 18 - source/pza/core/core.hxx | 29 - source/pza/core/device.cxx | 56 -- source/pza/core/device.hxx | 65 -- source/pza/core/device_factory.cxx | 21 - source/pza/core/device_factory.hxx | 27 - source/pza/core/field.hxx | 124 ---- source/pza/core/grouped_interface.hxx | 51 -- source/pza/core/interface.cxx | 46 -- source/pza/core/interface.hxx | 33 - source/pza/devices/bps.cxx | 33 - source/pza/devices/bps.hxx | 43 -- source/pza/interfaces/bps_chan_ctrl.cxx | 78 --- source/pza/interfaces/bps_chan_ctrl.hxx | 35 -- source/pza/interfaces/meter.cxx | 34 - source/pza/interfaces/meter.hxx | 20 - source/pza/utils/json.cxx | 112 ---- source/pza/utils/json.hxx | 18 - source/pza/utils/string.cxx | 8 - source/pza/utils/string.hxx | 11 - source/pza/utils/topic.cxx | 24 - source/pza/utils/topic.hxx | 25 - source/pza/version.hxx.in | 3 - source/utils/interface_group.cxx | 38 ++ source/utils/json_attribute.cxx | 42 ++ source/utils/json_attribute.hxx | 31 + source/utils/topic.cxx | 57 ++ source/utils/topic.hxx | 48 ++ test/CMakeLists.txt | 29 +- test/alias.cxx | 404 ------------ test/alias/folder_multiple/good.json | 9 - test/alias/folder_multiple/good2.json | 9 - .../alias/folder_multiple_duplicate/good.json | 9 - .../folder_multiple_duplicate/good2.json | 9 - test/alias/folder_partial_good/good.json | 9 - test/alias/folder_single/good.json | 9 - test/alias/good.json | 9 - test/connection.cxx | 176 ------ test/interface.cxx | 52 -- test/main.cxx | 8 - test/psu.cxx | 59 -- test/tree.json | 27 - test/{alias/empty.json => unit/attribute.cxx} | 0 .../bad.json => unit/client.cxx} | 0 test/unit/core.cxx | 32 + 105 files changed, 2698 insertions(+), 2821 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .github/workflows/build.yml create mode 100644 CHANGELOG.md create mode 100644 CMakePresets.json create mode 100644 cmake/compiler.cmake delete mode 100644 cmake/cppcheck.cmake create mode 100644 cmake/version.cmake create mode 100644 conan_profiles/linux_clang rename conan_profiles/{x86_64_Linux => linux_gcc} (50%) create mode 100644 conan_profiles/windows_gcc delete mode 100644 conan_profiles/x86_64_Cross_Windows delete mode 100644 examples/README.md create mode 100644 include/pza/core/client.hxx create mode 100644 include/pza/core/core.hxx create mode 100644 include/pza/core/device.hxx create mode 100644 include/pza/core/interface.hxx create mode 100644 include/pza/core/utils.hxx create mode 100644 include/pza/interfaces/ammeter.hxx create mode 100644 include/pza/interfaces/bpc.hxx create mode 100644 include/pza/interfaces/device.hxx create mode 100644 include/pza/interfaces/meter.hxx create mode 100644 include/pza/interfaces/platform.hxx create mode 100644 include/pza/interfaces/voltmeter.hxx delete mode 100755 scripts/build.sh create mode 100755 scripts/format.sh delete mode 100755 scripts/install_dependencies.sh create mode 100644 source/core/attribute.cxx create mode 100644 source/core/attribute.hxx create mode 100644 source/core/client.cxx create mode 100644 source/core/core.cxx create mode 100644 source/core/device.cxx create mode 100644 source/core/interface.cxx create mode 100644 source/core/interface.hxx create mode 100644 source/core/interface_factory.cxx create mode 100644 source/core/interface_factory.hxx create mode 100644 source/core/mqtt_service.hxx create mode 100644 source/core/scanner.cxx create mode 100644 source/core/scanner.hxx create mode 100644 source/core/version.cxx.in create mode 100644 source/interfaces/ammeter.cxx create mode 100644 source/interfaces/bpc.cxx create mode 100644 source/interfaces/device.cxx create mode 100644 source/interfaces/meter.cxx create mode 100644 source/interfaces/platform.cxx create mode 100644 source/interfaces/voltmeter.cxx delete mode 100644 source/pza/core/attribute.cxx delete mode 100644 source/pza/core/attribute.hxx delete mode 100644 source/pza/core/client.cxx delete mode 100644 source/pza/core/client.hxx delete mode 100644 source/pza/core/core.cxx delete mode 100644 source/pza/core/core.hxx delete mode 100644 source/pza/core/device.cxx delete mode 100644 source/pza/core/device.hxx delete mode 100644 source/pza/core/device_factory.cxx delete mode 100644 source/pza/core/device_factory.hxx delete mode 100644 source/pza/core/field.hxx delete mode 100644 source/pza/core/grouped_interface.hxx delete mode 100644 source/pza/core/interface.cxx delete mode 100644 source/pza/core/interface.hxx delete mode 100644 source/pza/devices/bps.cxx delete mode 100644 source/pza/devices/bps.hxx delete mode 100644 source/pza/interfaces/bps_chan_ctrl.cxx delete mode 100644 source/pza/interfaces/bps_chan_ctrl.hxx delete mode 100644 source/pza/interfaces/meter.cxx delete mode 100644 source/pza/interfaces/meter.hxx delete mode 100644 source/pza/utils/json.cxx delete mode 100644 source/pza/utils/json.hxx delete mode 100644 source/pza/utils/string.cxx delete mode 100644 source/pza/utils/string.hxx delete mode 100644 source/pza/utils/topic.cxx delete mode 100644 source/pza/utils/topic.hxx delete mode 100644 source/pza/version.hxx.in create mode 100644 source/utils/interface_group.cxx create mode 100644 source/utils/json_attribute.cxx create mode 100644 source/utils/json_attribute.hxx create mode 100644 source/utils/topic.cxx create mode 100644 source/utils/topic.hxx delete mode 100644 test/alias.cxx delete mode 100644 test/alias/folder_multiple/good.json delete mode 100644 test/alias/folder_multiple/good2.json delete mode 100644 test/alias/folder_multiple_duplicate/good.json delete mode 100644 test/alias/folder_multiple_duplicate/good2.json delete mode 100644 test/alias/folder_partial_good/good.json delete mode 100644 test/alias/folder_single/good.json delete mode 100644 test/alias/good.json delete mode 100644 test/connection.cxx delete mode 100644 test/interface.cxx delete mode 100644 test/main.cxx delete mode 100644 test/psu.cxx delete mode 100644 test/tree.json rename test/{alias/empty.json => unit/attribute.cxx} (100%) rename test/{alias/folder_partial_good/bad.json => unit/client.cxx} (100%) create mode 100644 test/unit/core.cxx diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..5645851 --- /dev/null +++ b/.clang-format @@ -0,0 +1,19 @@ +BasedOnStyle: LLVM +IndentWidth: 8 +TabWidth: 8 +UseTab: Always +BreakBeforeBraces: Linux +AllowShortIfStatementsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: false +IndentCaseLabels: false +ColumnLimit: 120 +AccessModifierOffset: -8 +NamespaceIndentation: None +PointerAlignment: Right +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +FixNamespaceComments: false +AlignAfterOpenBracket: Align +PackConstructorInitializers: Never diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..eb544e1 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: + clang-diagnostic-*, + clang-analyzer-*, + cppcoreguidelines-*, + modernize-*, + -modernize-use-trailing-return-type, + -cppcoreguidelines-non-private-member-variables-in-classes +WarningsAsErrors: '*' +HeaderFilterRegex: '.*' +--- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e3a0dfb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,90 @@ +name: CI Workflow + +on: [push, pull_request] + +jobs: + format: + runs-on: ubuntu-latest + container: + image: ghcr.io/panduza/pzacxx-build-img:latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Format + run: | + ./scripts/format.sh --check + + build: + runs-on: ubuntu-latest + container: + image: ghcr.io/panduza/pzacxx-build-img:latest + strategy: + fail-fast: false + matrix: + os: [linux, windows] + compiler: [gcc, clang] + build_type: [debug, release] + lib_type: [shared, static] + exclude: + - os: windows + compiler: clang + steps: + - name: Checkout + uses: actions/checkout@v4 + + - if: ${{ env.ACT }} + name: Hack container for local development + run: pacman -S --noconfirm nodejs + + - name: "cache" + uses: actions/cache@v3 + with: + path: ~/.conan2/p + key: conan-cache-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }}-${{ matrix.lib_type }} + + - id: build_type + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ matrix.build_type }} + + - name: "Install dependencies" + run: | + if [ "${{ matrix.lib_type }}" = "static" ]; then + shared="False" + else + shared="True" + fi + conan install -pr:b ./conan_profiles/linux_gcc -pr:h ./conan_profiles/${{ matrix.os }}_${{ matrix.compiler }} --build=missing -s build_type=${{ steps.build_type.outputs.capitalized }} -o shared=$shared . + + - name: "Build for ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.lib_type }}-${{ matrix.build_type }}" + run: | + if [ "${{ matrix.compiler }}" = "clang" ] && [ "${{ matrix.build_type }}" = "debug" ] && [ "${{ matrix.lib_type }}" = "shared" ]; then + tidy="True" + else + tidy="False" + fi + if [ "${{ matrix.os }}" = "windows" ]; then + cmake --preset ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.lib_type }} -DWITH_TIDY=$tidy -DWITH_TESTS=False + else + cmake --preset ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.lib_type }} -DWITH_TIDY=$tidy -DWITH_TESTS=True -DWITH_COVERAGE=True + fi + cmake --build --preset ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.lib_type }}-${{ matrix.build_type }} + + - name: "Test for ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.lib_type }}-${{ matrix.build_type }}" + if: ${{ matrix.os == 'Linux' }} + run: | + ctest --preset ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.lib_type }}-${{ matrix.build_type }} + + + - name: "Coverage" + if: ${{ matrix.os == 'linux' && matrix.compiler == 'clang' && matrix.lib_type == 'shared' && matrix.build_type == 'debug' }} + run: | + grcov . -s ./ -t html --llvm -o ./build/coverage/ --binary-path ./build/${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.lib_type }} --ignore '*test*' --ignore '*examples*' --ignore '*include*' --llvm-path=/usr/bin + + - name: "Upload coverage artifact" + if: ${{ matrix.os == 'linux' && matrix.compiler == 'clang' && matrix.lib_type == 'shared' && matrix.build_type == 'debug' }} + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: ./build/coverage/ diff --git a/.gitignore b/.gitignore index 6e8c37c..a887e6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ -build* +build/* examples/build* examples/CMakeUserPresets.json CMakeUserPresets.json +ConanPresets.json .vscode Testing conan_imports_manifest.txt +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f5776a8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# [0.2.0] +## Added +## Changed +## Fixed +## Removed + +# [0.1.0] +## Added +- Initial release +- Supports Bench Power Supply (BPS) devices diff --git a/CMakeLists.txt b/CMakeLists.txt index 66d1ebc..d2ce793 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,71 +1,34 @@ cmake_minimum_required(VERSION 3.25) -project(PZACXX VERSION 0.1.0) -set(LIBRARY_NAME pza-cxx) +include(cmake/version.cmake) +get_version_from_changelog() +project(PZACXX_LIBRARY VERSION ${PZACXX_VERSION} LANGUAGES CXX) + +set(LIBRARY_NAME pzacxx) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -include(cmake/cppcheck.cmake) - -add_compile_options(-Wall -Wextra) +include(cmake/compiler.cmake) +include(cmake/lib.cmake) -set(SPDLOG_FMT_EXTERNAL 1) find_package(spdlog REQUIRED) find_package(nlohmann_json REQUIRED) find_package(PahoMqttCpp REQUIRED) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") - find_package(GTest REQUIRED) - find_package(cppcheck REQUIRED) -endif() - -if (CMAKE_SYSTEM_NAME MATCHES "Windows") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--allow-multiple-definition") -elseif (CMAKE_SYSTEM_NAME MATCHES "Linux" AND NOT BUILD_SHARED_LIBS) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--allow-multiple-definition") -endif() - -if (CMAKE_SYSTEM_NAME MATCHES "Windows" AND NOT BUILD_SHARED_LIBS) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") -endif() - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -add_library(${LIBRARY_NAME}) add_subdirectory(source) -#add_subdirectory(test) -option(BUILD_EXAMPLES "Build examples" OFF) -if(BUILD_EXAMPLES) +option(WITH_EXAMPLES "Build examples" OFF) +if (WITH_EXAMPLES) add_subdirectory(examples) endif() -target_include_directories(${LIBRARY_NAME} PUBLIC - ${CMAKE_CURRENT_LIST_DIR} - ${CMAKE_BINARY_DIR} -) - -target_link_libraries(${LIBRARY_NAME} - $<$:PahoMqttCpp::paho-mqttpp3> - $<$>:PahoMqttCpp::paho-mqttpp3-static> - spdlog::spdlog - nlohmann_json::nlohmann_json -) - -if (CMAKE_SYSTEM_NAME MATCHES "Windows" AND BUILD_SHARED_LIBS) - message(STATUS "Copying DLLs from ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin to ${CMAKE_BINARY_DIR}/bin") - add_custom_command(TARGET ${LIBRARY_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin/*.dll - $ - ) +option(WITH_TESTS "Build tests" OFF) +if (WITH_TESTS) + enable_testing() + add_subdirectory(test) endif() - -set_target_properties(${LIBRARY_NAME} PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} -) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..7edfac2 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,4 @@ +{ + "version": 6, + "include": ["./ConanPresets.json"] +} diff --git a/cmake/compiler.cmake b/cmake/compiler.cmake new file mode 100644 index 0000000..d5961e6 --- /dev/null +++ b/cmake/compiler.cmake @@ -0,0 +1,25 @@ +include_guard() + +if (CMAKE_SYSTEM_NAME MATCHES "Windows") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--allow-multiple-definition") +elseif (CMAKE_SYSTEM_NAME MATCHES "Linux" AND NOT BUILD_SHARED_LIBS) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--allow-multiple-definition") +endif() + +if (CMAKE_SYSTEM_NAME MATCHES "Windows" AND NOT BUILD_SHARED_LIBS) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") +endif() + +option(WITH_TIDY "Enable clang-tidy" OFF) +if (WITH_TIDY) + find_program(CLANG_TIDY_EXECUTABLE clang-tidy REQUIRED) + set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXECUTABLE}) + set(CMAKE_C_CLANG_TIDY ${CLANG_TIDY_EXECUTABLE}) +endif() + +option(WITH_COVERAGE "Enable code coverage" OFF) +if (WITH_COVERAGE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") +endif() diff --git a/cmake/cppcheck.cmake b/cmake/cppcheck.cmake deleted file mode 100644 index 56ffbb1..0000000 --- a/cmake/cppcheck.cmake +++ /dev/null @@ -1,14 +0,0 @@ -if (CMAKE_SYSTEM_NAME MATCHES "Linux") - find_program(CPPCHECK_EXECUTABLE cppcheck) - if(CPPCHECK_EXECUTABLE) - add_custom_target(check - ${CPPCHECK_EXECUTABLE} --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction - --template "{file}:{line}:{severity}:{id}:{message}" - -I ${CMAKE_SOURCE_DIR}/source - ${CMAKE_SOURCE_DIR}/source - COMMENT "Running Cppcheck static analysis tool" - ) - else() - message(WARNING "Cppcheck not found, can't run static analysis") - endif() -endif() diff --git a/cmake/lib.cmake b/cmake/lib.cmake index 22897e7..77703a6 100644 --- a/cmake/lib.cmake +++ b/cmake/lib.cmake @@ -1,7 +1,31 @@ -set(CMAKE_DEBUG_POSTFIX -debug) -set_target_properties(${LIBRARY_NAME} - PROPERTIES - VERSION "${LIBRARY_VERSION}" - SOVERSION "${LIBRARY_VERSION_MAJOR}" - DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX} +include_guard() + +add_library(${LIBRARY_NAME}) + +target_link_libraries(${LIBRARY_NAME} + $<$:PahoMqttCpp::paho-mqttpp3> + $<$>:PahoMqttCpp::paho-mqttpp3-static> + spdlog::spdlog + nlohmann_json::nlohmann_json ) + +target_include_directories(${LIBRARY_NAME} PUBLIC + $ + $ +) + +set_target_properties(${LIBRARY_NAME} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) + +set_target_properties(${LIBRARY_NAME} PROPERTIES DEBUG_POSTFIX d) + +install(TARGETS ${LIBRARY_NAME} + EXPORT ${PROJECT_NAME}Targets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin +) + +install(DIRECTORY include/ DESTINATION include) diff --git a/cmake/version.cmake b/cmake/version.cmake new file mode 100644 index 0000000..858b045 --- /dev/null +++ b/cmake/version.cmake @@ -0,0 +1,27 @@ +include_guard() + +function(get_version_from_changelog) + file(STRINGS CHANGELOG.md FIRST_LINE LIMIT_COUNT 1) + + string(REGEX REPLACE "# \\[([0-9]+)\\.([0-9]+)\\.([0-9]+)\\].*" "\\1;\\2;\\3" VERSION_NUMBERS "${FIRST_LINE}") + list(GET VERSION_NUMBERS 0 MAJOR) + list(GET VERSION_NUMBERS 1 MINOR) + list(GET VERSION_NUMBERS 2 PATCH) + + set(PZACXX_VERSION_MAJOR ${MAJOR} PARENT_SCOPE) + set(PZACXX_VERSION_MINOR ${MINOR} PARENT_SCOPE) + set(PZACXX_VERSION_PATCH ${PATCH} PARENT_SCOPE) + set(PZACXX_VERSION "${MAJOR}.${MINOR}.${PATCH}" PARENT_SCOPE) + + find_package(Git REQUIRED) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + RESULT_VARIABLE result + OUTPUT_VARIABLE GITHASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if (result) + message(FATAL_ERROR "Failed to get git hash: ${result}") + endif() + set(PZACXX_VERSION_GITHASH ${GITHASH} PARENT_SCOPE) +endfunction() diff --git a/conan_profiles/linux_clang b/conan_profiles/linux_clang new file mode 100644 index 0000000..13745f5 --- /dev/null +++ b/conan_profiles/linux_clang @@ -0,0 +1,10 @@ +[settings] +os=Linux +arch=x86_64 +compiler=clang +compiler.version=16 +compiler.libcxx=libstdc++11 +compiler.cppstd=gnu17 + +[conf] +tools.build:compiler_executables={'c': 'clang', 'cpp': 'clang++' } diff --git a/conan_profiles/x86_64_Linux b/conan_profiles/linux_gcc similarity index 50% rename from conan_profiles/x86_64_Linux rename to conan_profiles/linux_gcc index 4721d8f..ad3ffca 100644 --- a/conan_profiles/x86_64_Linux +++ b/conan_profiles/linux_gcc @@ -4,3 +4,7 @@ arch=x86_64 compiler=gcc compiler.version=13 compiler.libcxx=libstdc++11 +compiler.cppstd=gnu17 + +[conf] +tools.build:compiler_executables={'c': 'gcc', 'cpp': 'g++' } diff --git a/conan_profiles/windows_gcc b/conan_profiles/windows_gcc new file mode 100644 index 0000000..fe099e3 --- /dev/null +++ b/conan_profiles/windows_gcc @@ -0,0 +1,19 @@ +[buildenv] +AR=x86_64-w64-mingw32-ar +AS=x86_64-w64-mingw32-as +RANLIB=x86_64-w64-mingw32-ranlib +CC=x86_64-w64-mingw32-gcc +CXX=x86_64-w64-mingw32-g++ +STRIP=x86_64-w64-mingw32-strip +RC=x86_64-w64-mingw32-windres + +[settings] +os=Windows +arch=x86_64 +compiler=gcc +compiler.version=12 +compiler.libcxx=libstdc++11 +compiler.cppstd=17 + +[conf] +tools.build:compiler_executables={"c": "x86_64-w64-mingw32-gcc", "cpp": "x86_64-w64-mingw32-g++"} diff --git a/conan_profiles/x86_64_Cross_Windows b/conan_profiles/x86_64_Cross_Windows deleted file mode 100644 index d7b63fe..0000000 --- a/conan_profiles/x86_64_Cross_Windows +++ /dev/null @@ -1,22 +0,0 @@ -target_host=x86_64-w64-mingw32 - -[env] -CHOST=$target_host -AR=$target_host-ar -AS=$target_host-as -RANLIB=$target_host-ranlib -CC=$target_host-gcc -CXX=$target_host-g++ -STRIP=$target_host-strip -RC=$target_host-windres - -[conf] -tools.build:compiler_executables={"cpp": "$target_host-g++", "c": "$target_host-gcc"} - -# We are cross building to Windows -[settings] -os=Windows -arch=x86_64 -compiler=gcc -compiler.version=13 -compiler.libcxx=libstdc++11 diff --git a/conanfile.py b/conanfile.py index 361a6ec..2a5d476 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,4 +1,4 @@ -from conans import ConanFile +from conan import ConanFile from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout import os import re @@ -7,34 +7,32 @@ class PzaCxx(ConanFile): name = "libpza-cxx" settings = "os", "compiler", "build_type", "arch" options = { - "shared": [True, False], - "build_examples": [True, False] + "shared": [True, False] } default_options = { - "shared": True, - "build_examples": True + "shared": True } - generators = "CMakeDeps", "CMakeToolchain", "virtualrunenv" - exports_sources = "CMakeLists.txt", "source/*", "version.h.in", "CHANGELOG.md", "test/*", "cmake/*", "examples/*", "LICENSE" def requirements(self): self.requires("paho-mqtt-cpp/[>=1.2.0]") - self.requires("spdlog/[>=1.11.0]") + self.requires("spdlog/[>=1.12.0]") self.requires("nlohmann_json/[>=3.11.2]") - if self.settings.os == "Linux": - self.requires("gtest/cci.20210126") - self.requires("cppcheck/[>=2.10]") def layout(self): - cmake_layout(self, build_folder=os.getcwd()) + self.folders.build_folder_vars = ['settings.os', 'settings.compiler', 'options.shared'] + cmake_layout(self, generator="Ninja Multi-Config") def configure(self): - self.options["*"].shared = self.options.shared + self.options["paho-mqtt-cpp"].shared = self.options.shared + self.options["spdlog"].shared = self.options.shared + self.options["nlohmann_json"].shared = self.options.shared def generate(self): tc = CMakeToolchain(self) - tc.variables["BUILD_EXAMPLES"] = self.options.build_examples + tc.generator = "Ninja Multi-Config" tc.filename = "pzacxx_toolchain.cmake" + tc.user_presets_path = 'ConanPresets.json' + tc.presets_prefix = "" tc.generate() deps = CMakeDeps(self) deps.generate() @@ -47,13 +45,3 @@ def build(self): def package(self): cmake = CMake(self) cmake.install() - - def imports(self): - if self.settings.os == "Windows" and self.options.shared: - folder = f"{self.build_folder}/bin" - self.copy("*.dll", dst=folder, src="bin") - mingw_dlls = ["libgcc_s_seh-1.dll", "libwinpthread-1.dll", "libstdc++-6.dll"] - mingw_dll_path = "/usr/x86_64-w64-mingw32/bin" - for dll in mingw_dlls: - self.copy(dll, dst=folder, src=mingw_dll_path) - diff --git a/docker/Dockerfile b/docker/Dockerfile index 9a3294a..27e12c2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM archlinux:base-devel-20231001.0.182270 +FROM archlinux:base-devel-20240101.0.204074 RUN echo -e "\n[multilib]\nInclude = /etc/pacman.d/mirrorlist" | sudo tee -a /etc/pacman.conf RUN pacman -Syu --noconfirm @@ -9,15 +9,18 @@ RUN pacman -Sy --noconfirm \ python \ python-pip \ wget \ - clang \ libunwind \ - wine + wine \ + gcc \ + clang \ + gtest \ + grcov \ + nodejs RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" -RUN pip install conan==1.60 -RUN conan profile new default --detect -RUN conan profile update settings.compiler.libcxx=libstdc++11 default +RUN pip install conan==2.0.13 +RUN conan profile detect RUN useradd -m mingw && echo "mingw:password" | chpasswd RUN echo "mingw ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/myuser @@ -30,4 +33,5 @@ RUN paru -S --noconfirm mingw-w64-cmake mingw-w64-zstd mingw-w64-zlib USER root RUN ln -s /usr/x86_64-w64-mingw32/lib/librpcrt4.a /usr/x86_64-w64-mingw32/lib/libRpcRT4.a RUN userdel -r mingw -WORKDIR / \ No newline at end of file + +RUN git config --system --add safe.directory '*' diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4703985..95aec93 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -5,14 +5,9 @@ set(examples foreach(example ${examples}) add_executable(${example} ${example}.cxx) set_target_properties(${example} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/bin" + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/examples) + set_source_files_properties(${example}.cxx PROPERTIES + SKIP_LINTING ON ) target_link_libraries(${example} ${LIBRARY_NAME}) - if (CMAKE_SYSTEM_NAME MATCHES "Windows" AND BUILD_SHARED_LIBS) - add_custom_command(TARGET ${example} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin/*.dll - $ - ) - endif() endforeach() diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 64bbdc3..0000000 --- a/examples/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Examples - -TBD \ No newline at end of file diff --git a/examples/bps.cxx b/examples/bps.cxx index 4a84205..f1529a3 100644 --- a/examples/bps.cxx +++ b/examples/bps.cxx @@ -1,40 +1,63 @@ #include -#include +#include +#include +#include +#include -int main(int argc, char** argv) +static const std::string addr = "localhost"; +static const unsigned int port = 1883; + +int main() { - if (argc != 5) { - std::cerr << "Usage: " << argv[0] << "
" << std::endl; - return -1; - } + pza::core::set_log_level(pza::core::log_level::debug); + + pza::core::get_log_level(); + + pza::client::s_ptr cli = std::make_shared(addr, port); + + if (cli->connect() < 0) + return -1; + + if (cli->register_devices() < 0) + return -1; + + for (auto &dev : cli->get_devices()) + spdlog::info("device: {}", dev->get_name()); + + auto bps = cli->get_device("default", "Panduza_FakeBps"); + + for (auto itf : bps->get_interfaces()) + spdlog::info("interface: {}", itf->get_name()); - const char *address = argv[1]; - int port = std::stoi(argv[2]); - const char *group = argv[3]; - const char *bps_name = argv[4]; + auto vm = cli->get_interface("default", "Panduza_FakeBps", "channel", 0, "vm"); + if (!vm) { + spdlog::error("voltmeter not found"); + return -1; + } - pza::core::set_log_level(pza::core::log_level::debug); + auto ctrl = cli->get_interface("default", "Panduza_FakeBps", "channel", 0, "ctrl"); + if (!ctrl) { + spdlog::error("ctrl not found"); + return -1; + } - pza::client::ptr cli = std::make_shared(address, port); + auto am = cli->get_interface("default", "Panduza_FakeBps", "channel", 0, "am"); + if (!am) { + spdlog::error("ammeter not found"); + return -1; + } - if (cli->connect() == -1) - return -1; + ctrl->set_enable(true); + ctrl->set_voltage(3.3); + ctrl->set_current(0.1); - pza::bps::ptr bps = std::make_shared(group, bps_name); + vm->register_measure_callback([&]() { spdlog::info("voltage: {}", vm->get_measure()); }); - if (cli->register_device(bps) == -1) - return -1; + am->register_measure_callback([&]() { spdlog::info("current: {}", am->get_measure()); }); - for (size_t i = 0; i < bps->get_num_channels(); i++) { - auto bps_channel = bps->channel[i]; - spdlog::info("Channel {}:", i); - bps_channel->ctrl.set_voltage(-7.3); - bps_channel->ctrl.set_current(1.0); - bps_channel->ctrl.set_enable(false); - spdlog::info(" Voltage: {}", bps_channel->voltmeter.get_measure()); - spdlog::info(" Current: {}", bps_channel->ampermeter.get_measure()); - spdlog::info(" Enabled: {}", bps_channel->ctrl.get_enable()); - } + cli->disconnect(); - return cli->disconnect(); + // while (true) + // ; + return 0; } diff --git a/include/pza/core/client.hxx b/include/pza/core/client.hxx new file mode 100644 index 0000000..7831bfa --- /dev/null +++ b/include/pza/core/client.hxx @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include + +#include + +struct client_impl; + +namespace pza +{ +class client +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit client(const std::string &addr, int port, std::optional id = std::nullopt); + client(const client &) = delete; + client &operator=(const client &) = delete; + client(client &&) = delete; + client &operator=(client &&) = delete; + ~client(); + + int connect(); + int disconnect(); + [[nodiscard]] bool is_connected() const; + + [[nodiscard]] const std::string &get_addr() const; + [[nodiscard]] const std::string &get_id() const; + [[nodiscard]] int get_port() const; + + void set_connection_timeout(unsigned int timeout_ms); + [[nodiscard]] unsigned int get_connection_timeout() const; + + device::s_ptr register_device(const std::string &group, const std::string &name, + unsigned int timeout_ms = device_timeout_default); + int register_devices(unsigned int timeout_ms = devices_timeout_default); + + [[nodiscard]] device::s_ptr get_device(const std::string &group, const std::string &name) const; + [[nodiscard]] std::vector get_devices() const; + [[nodiscard]] std::vector get_devices_in_group(const std::string &group) const; + [[nodiscard]] std::set get_groups() const; + + [[nodiscard]] itf_base::s_ptr get_interface(const std::string &group, const std::string &name, + const std::string &interface_group, unsigned int idx, + const std::string &interface_name) const; + [[nodiscard]] itf_base::s_ptr get_interface(const std::string &group, const std::string &name, + const std::string &interface_name) const; + + template + [[nodiscard]] std::shared_ptr get_interface(const std::string &group, const std::string &name, + const std::string &interface_group, unsigned int idx, + const std::string &interface_name) const + { + return std::dynamic_pointer_cast(get_interface(group, name, interface_group, idx, interface_name)); + } + + template + [[nodiscard]] std::shared_ptr get_interface(const std::string &group, const std::string &name, + const std::string &interface_name) const + { + return std::dynamic_pointer_cast(get_interface(group, name, interface_name)); + } + + [[nodiscard]] std::vector get_interfaces() const; + +private: + static constexpr unsigned int platforms_timeout_default = 500; + static constexpr unsigned int device_timeout_default = 500; + static constexpr unsigned int devices_timeout_default = device_timeout_default * 5; + + std::unique_ptr _impl; +}; +}; diff --git a/include/pza/core/core.hxx b/include/pza/core/core.hxx new file mode 100644 index 0000000..b9fdf92 --- /dev/null +++ b/include/pza/core/core.hxx @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace pza::core +{ +enum class log_level : int { + trace = spdlog::level::trace, + debug = spdlog::level::debug, + info = spdlog::level::info, + warn = spdlog::level::warn, + err = spdlog::level::err, + critical = spdlog::level::critical, + off = spdlog::level::off +}; + +void set_log_level(log_level level); +log_level get_log_level(); + +std::string get_version(); +std::string get_version_githash(); +unsigned int get_version_major(); +unsigned int get_version_minor(); +unsigned int get_version_patch(); +}; diff --git a/include/pza/core/device.hxx b/include/pza/core/device.hxx new file mode 100644 index 0000000..a08ddf0 --- /dev/null +++ b/include/pza/core/device.hxx @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +#include + +struct device_impl; +class mqtt_service; +struct device_info; + +namespace pza +{ +class client; + +class device +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit device(mqtt_service *mqtt, struct device_info &info); + device(const device &) = delete; + device &operator=(const device &) = delete; + device(device &&) = delete; + device &operator=(device &&) = delete; + ~device(); + + [[nodiscard]] const std::string &get_name() const; + [[nodiscard]] const std::string &get_group() const; + [[nodiscard]] const std::string &get_model() const; + [[nodiscard]] const std::string &get_manufacturer() const; + [[nodiscard]] const std::string &get_family() const; + + [[nodiscard]] unsigned int get_number_of_interfaces() const; + + [[nodiscard]] itf_base::s_ptr get_interface(const std::string &name) const; + + template std::shared_ptr get_interface(const std::string &name) const + { + return std::dynamic_pointer_cast(get_interface(name)); + } + + [[nodiscard]] itf_base::s_ptr get_interface(const std::string &interface_group, unsigned int idx, + const std::string &name) const; + + template + std::shared_ptr get_interface(const std::string &interface_group, unsigned int idx, + const std::string &name) const + { + return std::dynamic_pointer_cast(get_interface(interface_group, idx, name)); + } + + [[nodiscard]] std::vector get_interfaces_name() const; + + [[nodiscard]] std::vector get_interfaces_in_group(const std::string &group) const; + + [[nodiscard]] std::vector get_interfaces_in_group(const std::string &group, + unsigned int index) const; + + [[nodiscard]] std::set get_interface_groups() const; + + [[nodiscard]] size_t get_interface_groups_count() const; + + [[nodiscard]] std::vector get_interfaces() const; + +private: + std::unique_ptr _impl; +}; +}; diff --git a/include/pza/core/interface.hxx b/include/pza/core/interface.hxx new file mode 100644 index 0000000..310afa6 --- /dev/null +++ b/include/pza/core/interface.hxx @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +struct itf_impl; +class mqtt_service; +class attribute; + +namespace pza +{ +struct itf_info { + std::string bench; + std::string device_name; + std::string name; + std::string type; +}; + +// Can't use the name "interface" because it's a reserved keyford for Windows in C++ +class itf_base +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + using attribute_ptr = std::shared_ptr; + + virtual ~itf_base(); + itf_base(const itf_base &) = delete; + itf_base &operator=(const itf_base &) = delete; + itf_base(itf_base &&) = delete; + itf_base &operator=(itf_base &&) = delete; + + [[nodiscard]] const std::string &get_name() const; + [[nodiscard]] const std::string &get_type() const; + +protected: + explicit itf_base(mqtt_service *mqtt, itf_info &info); + + std::unique_ptr _impl; +}; +}; diff --git a/include/pza/core/utils.hxx b/include/pza/core/utils.hxx new file mode 100644 index 0000000..87bf4a4 --- /dev/null +++ b/include/pza/core/utils.hxx @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace pza::utils +{ + +struct itf_group_info { + std::string group_name; + unsigned int index = 0; + std::string interface_name; +}; + +bool is_interface_grouped(const std::string &itf_name); + +int get_grouped_interface_info(const std::string &itf_name, itf_group_info &info); + +std::string format_interface_group(const std::string &group_name, unsigned int index, + const std::string &interface_name); +}; diff --git a/include/pza/interfaces/ammeter.hxx b/include/pza/interfaces/ammeter.hxx new file mode 100644 index 0000000..246367e --- /dev/null +++ b/include/pza/interfaces/ammeter.hxx @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace pza::itf +{ +class ammeter : public meter +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit ammeter(mqtt_service *mqtt, itf_info &info); + ammeter(const ammeter &) = delete; + ammeter(ammeter &&) = delete; + ammeter &operator=(const ammeter &) = delete; + ammeter &operator=(ammeter &&) = delete; + ~ammeter() override; +}; +}; diff --git a/include/pza/interfaces/bpc.hxx b/include/pza/interfaces/bpc.hxx new file mode 100644 index 0000000..320daed --- /dev/null +++ b/include/pza/interfaces/bpc.hxx @@ -0,0 +1,43 @@ +#pragma once + +#include + +namespace pza::itf +{ +class bpc : public itf_base +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit bpc(mqtt_service *mqtt, itf_info &info); + bpc(const bpc &) = delete; + bpc(bpc &&) = delete; + bpc &operator=(const bpc &) = delete; + bpc &operator=(bpc &&) = delete; + ~bpc() override; + + int set_voltage(double volts); + int set_current(double amps); + int set_enable(bool enable); + + bool get_enable(); + double get_min_voltage(); + double get_max_voltage(); + double get_min_current(); + double get_max_current(); + double get_preset_voltage(); + double get_preset_current(); + unsigned int get_num_decimals_voltage(); + unsigned int get_num_decimals_current(); + + void register_enable_callback(const std::function &callback); + void remove_enable_callback(const std::function &callback); + +private: + attribute_ptr _voltage; + attribute_ptr _current; + attribute_ptr _enable; +}; +}; diff --git a/include/pza/interfaces/device.hxx b/include/pza/interfaces/device.hxx new file mode 100644 index 0000000..4553f01 --- /dev/null +++ b/include/pza/interfaces/device.hxx @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace pza::itf +{ +class device : public itf_base +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit device(mqtt_service *mqtt, itf_info &info); + device(const device &) = delete; + device(device &&) = delete; + device &operator=(const device &) = delete; + device &operator=(device &&) = delete; + ~device() override; + + [[nodiscard]] const std::string &get_model() const; + [[nodiscard]] const std::string &get_manufacturer() const; + [[nodiscard]] const std::string &get_family() const; + +private: + attribute_ptr _identity; +}; +}; diff --git a/include/pza/interfaces/meter.hxx b/include/pza/interfaces/meter.hxx new file mode 100644 index 0000000..b17dff0 --- /dev/null +++ b/include/pza/interfaces/meter.hxx @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include + +namespace pza::itf +{ +class meter : public itf_base +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit meter(mqtt_service *mqtt, itf_info &info); + meter(const meter &) = delete; + meter(meter &&) = delete; + meter &operator=(const meter &) = delete; + meter &operator=(meter &&) = delete; + ~meter() override; + + double get_measure(); + + void register_measure_callback(const std::function &callback); + void remove_measure_callback(const std::function &callback); + +private: + attribute_ptr _measure; +}; +}; diff --git a/include/pza/interfaces/platform.hxx b/include/pza/interfaces/platform.hxx new file mode 100644 index 0000000..a3c906a --- /dev/null +++ b/include/pza/interfaces/platform.hxx @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace pza::itf +{ +class platform : public itf_base +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit platform(mqtt_service *mqtt, itf_info &info); + platform(const platform &) = delete; + platform(platform &&) = delete; + platform &operator=(const platform &) = delete; + platform &operator=(platform &&) = delete; + ~platform() override; + +private: +}; +}; diff --git a/include/pza/interfaces/voltmeter.hxx b/include/pza/interfaces/voltmeter.hxx new file mode 100644 index 0000000..5295ad6 --- /dev/null +++ b/include/pza/interfaces/voltmeter.hxx @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace pza::itf +{ +class voltmeter : public meter +{ +public: + using s_ptr = std::shared_ptr; + using u_ptr = std::unique_ptr; + using w_ptr = std::weak_ptr; + + explicit voltmeter(mqtt_service *mqtt, itf_info &info); + voltmeter(const voltmeter &) = delete; + voltmeter(voltmeter &&) = delete; + voltmeter &operator=(const voltmeter &) = delete; + voltmeter &operator=(voltmeter &&) = delete; + ~voltmeter() override; +}; +}; diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index f6fc9d4..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash - -TARGET="Linux" -LIB_MODE="Shared" -BUILD_MODE="Debug" -PROJECT_ROOT_DIR="$(dirname "$(realpath "$0")")/.." -CONAN_PROFILES_DIR="$PROJECT_ROOT_DIR/conan_profiles" -EXAMPLES="ON" - -TEMP=$(getopt -o t:l:b:h --long target:,lib:,build:,help,no-examples -n "$0" -- "$@") -if [ $? != 0 ]; then - echo "Error processing arguments." >&2 - exit 1 -fi - -eval set -- "$TEMP" - -while true; do - case "$1" in - -t|--target) - TARGET="$(tr '[:lower:]' '[:upper:]' <<< ${2:0:1})${2:1}" - shift 2 - ;; - -l|--lib) - LIB_MODE="$(tr '[:lower:]' '[:upper:]' <<< ${2:0:1})${2:1}" - shift 2 - ;; - -b|--build) - BUILD_MODE="$(tr '[:lower:]' '[:upper:]' <<< ${2:0:1})${2:1}" - shift 2 - ;; - --no-examples) - EXAMPLES="OFF" - shift - ;; - -h|--help) - echo "Usage: $0 [-t ] [-l ] [-b ] [-h]" - echo " -t --target Target platform (Windows or Linux). Default is Linux" - echo " -l --lib Library mode (Static or Shared). Default is Shared" - echo " -b --build Build mode (Debug or Release). Default is Debug" - echo " -h --help Display this help message" - exit 0 - ;; - --) - shift - break - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done - -if [ "$TARGET" == "Linux" ]; then - BUILD_DIR="build" - PROFILE_BUILD="x86_64_Linux" - PROFILE_HOST="x86_64_Linux" -elif [ "$TARGET" == "Windows" ]; then - BUILD_DIR="buildwin" - PROFILE_BUILD="x86_64_Linux" - PROFILE_HOST="x86_64_Cross_Windows" -else - echo "Target not supported: $TARGET" - exit 1 -fi - -if [ "$LIB_MODE" == "Static" ]; then - BUILD_DIR="${BUILD_DIR}_static" -elif [ "$LIB_MODE" != "Shared" ]; then - echo "Library mode not supported: $LIB_MODE" - exit 1 -fi - -FULL_BUILD_DIR="$PROJECT_ROOT_DIR/$BUILD_DIR" -FULL_PROFILE_BUILD="$CONAN_PROFILES_DIR/$PROFILE_BUILD" -FULL_PROFILE_HOST="$CONAN_PROFILES_DIR/$PROFILE_HOST" - -echo "Configuration:" -echo " Target : $TARGET" -echo " Lib Mode : $LIB_MODE" -echo " Build Mode : $BUILD_MODE" - -if [ ! -d "$FULL_BUILD_DIR" ]; then - echo "Build directory not found: $FULL_BUILD_DIR" - echo "Please run install_dependencies.sh first" - exit 1 -fi - -cd $FULL_BUILD_DIR -cmake \ - -DCMAKE_TOOLCHAIN_FILE=./$BUILD_MODE/generators/pzacxx_toolchain.cmake \ - -DCMAKE_BUILD_TYPE=$BUILD_MODE \ - -DBUILD_EXAMPLES=$EXAMPLES \ - .. -cmake --build . --config $BUILD_MODE --parallel ${nproc} diff --git a/scripts/docker_pull.sh b/scripts/docker_pull.sh index 90ff9ec..3b85030 100755 --- a/scripts/docker_pull.sh +++ b/scripts/docker_pull.sh @@ -1,3 +1,3 @@ #!/bin/bash -docker pull ghcr.io/panduza/pzacx-build-img:latest \ No newline at end of file +docker pull ghcr.io/panduza/pzacxx-build-img:latest diff --git a/scripts/docker_registry.sh b/scripts/docker_registry.sh index 1836595..6f0eaf4 100755 --- a/scripts/docker_registry.sh +++ b/scripts/docker_registry.sh @@ -1,6 +1,6 @@ #!/bin/bash -IMAGE_NAME="pzacx-build-img" +IMAGE_NAME="pzacxx-build-img" PROJECT_ROOT_DIR="$(dirname "$(realpath "$0")")/.." TEMP=$(getopt -o u:p:h --long username:,password:,help -n "$0" -- "$@") @@ -54,5 +54,5 @@ fi docker login ghcr.io -u $USERNAME -p $PASSWORD || exit 1 docker build -t $IMAGE_NAME $PROJECT_ROOT_DIR/docker || exit 1 -docker tag $IMAGE_NAME:latest ghcr.io/panduza/$IMAGE_NAME:latest || exit 1 -docker push ghcr.io/panduza/$IMAGE_NAME:latest \ No newline at end of file +docker tag $IMAGE_NAME ghcr.io/panduza/$IMAGE_NAME:latest +docker push ghcr.io/panduza/$IMAGE_NAME:latest diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 0000000..1099940 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +CLANG_FORMAT_PATH=$(which clang-format) +SOURCE_FOLDERS="source include examples" + +PROJECT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +SOURCE_PATHS=$(echo $SOURCE_FOLDERS | tr " " "\n" | sed "s|^|$PROJECT_DIR/|") +SOURCE_FILES=$(find $SOURCE_PATHS -name "*.h" -o -name "*.c" -o -name "*.cpp" -o -name "*.hpp" -o -name "*.hxx" -o -name "*.cxx") + +if [ -z "$CLANG_FORMAT_PATH" ]; then + echo "clang-format not found. Please install clang-format and add it to your PATH or set the CLANG_FORMAT_PATH environment variable." + exit 1 +fi + +# get options for --format and --check +FORMAT_OPTION=false +CHECK_OPTION=false +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + --format) + FORMAT_OPTION=true + shift + shift + ;; + --check) + CHECK_OPTION=true + shift + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +if [ "$FORMAT_OPTION" == false ] && [ "$CHECK_OPTION" == false ]; then + echo "No options provided. Please provide either --format or --check." + exit 1 +fi + +if [ "$FORMAT_OPTION" == true ] +then + echo "Formatting code..." + $CLANG_FORMAT_PATH --style=file -i $SOURCE_FILES +fi + +if [ "$CHECK_OPTION" == true ] +then + echo "Checking code formatting..." + $CLANG_FORMAT_PATH --style=file -output-replacements-xml $SOURCE_FILES | grep " /dev/null && echo "Error! Code is not well formatted." && exit 1 || echo "Format OK" && exit 0 +fi diff --git a/scripts/install_dependencies.sh b/scripts/install_dependencies.sh deleted file mode 100755 index ad273dc..0000000 --- a/scripts/install_dependencies.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash - -TARGET="Linux" -LIB_MODE="Shared" -BUILD_MODE="Debug" -PROJECT_ROOT_DIR="$(dirname "$(realpath "$0")")/.." -CONAN_PROFILES_DIR="$PROJECT_ROOT_DIR/conan_profiles" - -TEMP=$(getopt -o t:l:b:h --long target:,lib:,build:,help -n "$0" -- "$@") -if [ $? != 0 ]; then - echo "Error processing arguments." >&2 - exit 1 -fi - -eval set -- "$TEMP" - -while true; do - case "$1" in - -t|--target) - TARGET="$(tr '[:lower:]' '[:upper:]' <<< ${2:0:1})${2:1}" - shift 2 - ;; - -l|--lib) - LIB_MODE="$(tr '[:lower:]' '[:upper:]' <<< ${2:0:1})${2:1}" - shift 2 - ;; - -b|--build) - BUILD_MODE="$(tr '[:lower:]' '[:upper:]' <<< ${2:0:1})${2:1}" - shift 2 - ;; - -h|--help) - echo "Usage: $0 [-t ] [-l ] [-b ] [-h]" - echo " -t --target Target platform (Windows or Linux). Default is Linux" - echo " -l --lib Library mode (Static or Shared). Default is Shared" - echo " -b --build Build mode (Debug or Release). Default is Debug" - echo " -h --help Display this help message" - exit 0 - ;; - --) - shift - break - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done - -if [ "$TARGET" == "Linux" ]; then - BUILD_DIR="build" - PROFILE_BUILD="x86_64_Linux" - PROFILE_HOST="x86_64_Linux" -elif [ "$TARGET" == "Windows" ]; then - BUILD_DIR="buildwin" - PROFILE_BUILD="x86_64_Linux" - PROFILE_HOST="x86_64_Cross_Windows" -else - echo "Target not supported: $TARGET" - exit 1 -fi - -if [ "$LIB_MODE" == "Static" ]; then - BUILD_DIR="${BUILD_DIR}_static" -fi - -FULL_BUILD_DIR="$PROJECT_ROOT_DIR/$BUILD_DIR" -FULL_PROFILE_BUILD="$CONAN_PROFILES_DIR/$PROFILE_BUILD" -FULL_PROFILE_HOST="$CONAN_PROFILES_DIR/$PROFILE_HOST" - -echo "Configuration:" -echo " Target : $TARGET" -echo " Lib Mode : $LIB_MODE" -echo " Build Mode : $BUILD_MODE" - -mkdir -p $FULL_BUILD_DIR -cd $FULL_BUILD_DIR -conan install \ - -s build_type=$BUILD_MODE \ - -o shared=$( [ "$LIB_MODE" == "Shared" ] && echo "True" || echo "False" ) \ - --build=missing \ - --profile:b $FULL_PROFILE_BUILD \ - --profile:h $FULL_PROFILE_HOST \ - --install-folder=$FULL_BUILD_DIR \ - $PROJECT_ROOT_DIR diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index efaa9ca..4ada899 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -1,33 +1,27 @@ -file(GLOB_RECURSE HEADERS ${CMAKE_SOURCE_DIR}/source/*.hxx) - -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/pza/version.hxx.in - ${CMAKE_CURRENT_BINARY_DIR}/pza/version.hxx) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/core/version.cxx.in + ${CMAKE_CURRENT_BINARY_DIR}/version.cxx) target_sources(${LIBRARY_NAME} - PRIVATE - - pza/core/core.cxx - pza/core/client.cxx - pza/core/device.cxx - pza/core/device_factory.cxx - pza/core/interface.cxx - pza/core/attribute.cxx - - pza/utils/json.cxx - pza/utils/string.cxx - pza/utils/topic.cxx - - pza/devices/bps.cxx - - pza/interfaces/meter.cxx - pza/interfaces/bps_chan_ctrl.cxx -) - -target_include_directories(${LIBRARY_NAME} PUBLIC - ${CMAKE_CURRENT_LIST_DIR} - ${CMAKE_CURRENT_BINARY_DIR} -) - -install(TARGETS ${LIBRARY_NAME} - FILE_SET HEADERS + PRIVATE + + core/core.cxx + core/client.cxx + core/scanner.cxx + core/device.cxx + core/interface.cxx + core/interface_factory.cxx + core/attribute.cxx + + utils/interface_group.cxx + utils/json_attribute.cxx + utils/topic.cxx + + interfaces/device.cxx + interfaces/platform.cxx + interfaces/meter.cxx + interfaces/voltmeter.cxx + interfaces/ammeter.cxx + interfaces/bpc.cxx + + ${CMAKE_CURRENT_BINARY_DIR}/version.cxx ) diff --git a/source/core/attribute.cxx b/source/core/attribute.cxx new file mode 100644 index 0000000..ff3e011 --- /dev/null +++ b/source/core/attribute.cxx @@ -0,0 +1,50 @@ +#include "attribute.hxx" + +attribute::attribute(std::string name) + : _name(std::move(name)) +{ +} + +attribute::~attribute() = default; + +void attribute::on_message(mqtt::const_message_ptr msg) +{ + auto json = json_attribute(_name); + + if (json.parse(msg->get_payload()) < 0) { + spdlog::error("attribute::on_message: failed to parse payload"); + return; + } + + for (auto &field : _fields) { + auto &name = field.first; + auto &type = field.second; + + if (std::holds_alternative(type)) { + _set_field(json, name); + } else if (std::holds_alternative(type)) { + _set_field(json, name); + } else if (std::holds_alternative(type)) { + _set_field(json, name); + } else if (std::holds_alternative(type)) { + _set_field(json, name); + } else if (std::holds_alternative(type)) { + _set_field(json, name); + } else { + spdlog::error("attribute::on_message: unknown field type"); + } + } + + _cv.notify_all(); + + for (auto &cb : _callbacks) { + cb(); + } +} + +void attribute::remove_callback(const std::function &cb) +{ + _callbacks.remove_if([&](const std::function &f) { + return f.target_type() == cb.target_type() && f.target() == cb.target(); + }); +} diff --git a/source/core/attribute.hxx b/source/core/attribute.hxx new file mode 100644 index 0000000..5973727 --- /dev/null +++ b/source/core/attribute.hxx @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "../utils/json_attribute.hxx" + +class attribute +{ +public: + using s_ptr = std::shared_ptr; + + explicit attribute(std::string name); + attribute(const attribute &) = delete; + attribute(attribute &&) = delete; + attribute &operator=(const attribute &) = delete; + attribute &operator=(attribute &&) = delete; + ~attribute(); + + const std::string &get_name() const + { + return _name; + } + + template void register_field(const std::string &name) + { + _fields[name] = T(); + } + + template const T &get_field(const std::string &name) + { + try { + return std::get(_fields[name]); + } catch (const std::bad_variant_access &e) { + spdlog::error("attribute::get_field: {}", e.what()); + throw; + } + } + + template int set_field(const std::string &field, const T &val) + { + nlohmann::json data; + std::mutex mtx; + std::unique_lock lock(mtx); + + data[_name][field] = val; + + if (_msg_cb(data) < 0) { + spdlog::error("attribute::set: failed to send message"); + return -1; + } + + if (_cv.wait_for(lock, std::chrono::seconds(3), [&]() { return std::get(_fields[field]) == val; }) == + false) { + spdlog::error("attribute::set: timed out waiting for " + "value to be set"); + return -1; + } + return 0; + } + + void on_message(mqtt::const_message_ptr msg); + + void register_callback(const std::function &cb) + { + _callbacks.push_back(cb); + } + void remove_callback(const std::function &cb); + + void set_msg_callback(const std::function &cb) + { + _msg_cb = cb; + } + +private: + using field_types = std::variant; + + template void _set_field(json_attribute &json, const std::string &name) + { + T val; + if (json.get(name, val) < 0) { + spdlog::error("attribute::on_message: failed to get field {}", name); + return; + } + std::get(_fields[name]) = val; + } + + std::string _name; + std::unordered_map _fields; + std::list> _callbacks; + std::function _msg_cb; + std::condition_variable _cv; +}; diff --git a/source/core/client.cxx b/source/core/client.cxx new file mode 100644 index 0000000..f8f941d --- /dev/null +++ b/source/core/client.cxx @@ -0,0 +1,580 @@ +#include +#include + +#include +#include +#include + +#include +#include + +#include "../utils/json_attribute.hxx" +#include "../utils/topic.hxx" +#include "attribute.hxx" +#include "interface_factory.hxx" +#include "mqtt_service.hxx" +#include "scanner.hxx" + +using namespace pza; + +static constexpr unsigned int conn_timeout_default_ms = 2000; +static constexpr unsigned int msg_timeout_default_ms = 500; +static constexpr unsigned int platforms_timeout_ms = 500; + +struct device_info { + std::string name; + std::string group; + std::string model; + std::string manufacturer; + std::string family; + unsigned int number_of_interfaces = 0; +}; + +struct client_impl : mqtt_service { + explicit client_impl(const std::string &addr, int port, std::optional id = std::nullopt); + + int connect(); + int disconnect(); + bool is_connected() const + { + return _paho_client->is_connected(); + } + + const std::string &get_addr() const + { + return _addr; + } + const std::string &get_id() const + { + return _id; + } + int get_port() const + { + return _port; + } + + void set_connection_timeout(unsigned int timeout) + { + _conn_timeout = timeout; + } + unsigned int get_connection_timeout() const + { + return _conn_timeout; + } + + int publish(const std::string &topic, const std::string &payload) override; + int publish(mqtt::const_message_ptr msg) override; + int subscribe(const std::string &topic, const std::function &cb) override; + int unsubscribe(const std::string &topic) override; + + void connection_lost(const std::string &cause); + void message_arrived(mqtt::const_message_ptr msg); + + int scan_platforms(unsigned int timeout_ms); + int scan_device(const std::string &group, const std::string &name, unsigned int timeout_ms); + int scan_devices(unsigned int timeout_ms); + + device::s_ptr create_device(const std::string &group, const std::string &name); + device::s_ptr register_device(const std::string &group, const std::string &name, unsigned int timeout_ms); + int register_devices(unsigned int timeout_ms); + + device::s_ptr get_device(const std::string &group, const std::string &name) const; + std::vector get_devices() const; + std::vector get_devices_in_group(const std::string &group) const; + + std::set get_groups() const; + + // std::function for on_device_info + std::function on_device_info; + + mqtt::async_client::ptr_t _paho_client; + unsigned int _conn_timeout = conn_timeout_default_ms; + std::string _addr; + int _port; + std::string _id; + std::mutex _mtx; + std::unordered_map> _listeners; + std::unordered_map _devices; + std::set _platforms_scanned; + std::unordered_map _devices_scanned; + unsigned int _device_count = 0; +}; + +client_impl::client_impl(const std::string &addr, int port, std::optional id) + : _addr(addr), + _port(port) + +{ + std::string url = "tcp://" + addr + ":" + std::to_string(port); + + if (id.has_value() == false) { + constexpr int range = 1000000; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, range); + _id = "pza_" + std::to_string(dis(gen)); + spdlog::warn("no client id provided, using generated id: {}", _id); + } else + _id = id.value(); + + auto message_arrived = [&](mqtt::const_message_ptr msg) { + spdlog::trace("message arrived on topic: {}", msg->get_topic()); + + if (_listeners.count(msg->get_topic()) > 0) { + _listeners[msg->get_topic()](msg); + return; + } + + for (auto &it : _listeners) { + if (topic::topic_matches(msg->get_topic(), it.first)) { + it.second(msg); + } + } + }; + + auto connection_lost = [&](const std::string &cause) { spdlog::error("connection lost: {}", cause); }; + + on_device_info = [&](mqtt::const_message_ptr msg) { + std::string base_topic = msg->get_topic().substr(4, msg->get_topic().find("/device/atts/info") - 4); + + spdlog::trace("received device info: {} {}", msg->get_topic(), msg->get_payload_str()); + _devices_scanned.emplace(base_topic, msg->get_payload_str()); + }; + + _paho_client = std::make_unique(url, _id); + _paho_client->set_message_callback(message_arrived); + _paho_client->set_connection_lost_handler(connection_lost); + spdlog::trace("created client with id: {}", _id); +} + +int client_impl::connect() +{ + mqtt::connect_options connOpts; + constexpr int interval = 20; + + spdlog::debug("Attempting connection to {}...", _addr); + + connOpts.set_keep_alive_interval(interval); + connOpts.set_clean_session(true); + + try { + _paho_client->connect(connOpts)->wait_for(std::chrono::milliseconds(_conn_timeout)); + } catch (const mqtt::exception &exc) { + spdlog::error("Client {} failed to connect on {}:{}", _id, _addr, _port); + return -1; + } + + spdlog::info("connected to {}:{}", _addr, _port); + return 0; +} + +int client_impl::disconnect() +{ + spdlog::debug("Attempting to disconnect from {}...", _addr); + + try { + _paho_client->disconnect()->wait_for(std::chrono::milliseconds(_conn_timeout)); + } catch (const mqtt::exception &exc) { + spdlog::error("Client {} failed to disconnect on {}:{}", _id, _addr, _port); + return -1; + } + spdlog::info("disconnected from {}", _addr); + return 0; +} + +void client_impl::connection_lost(const std::string &cause) +{ + spdlog::error("connection lost: {}", cause); +} + +int client_impl::publish(const std::string &topic, const std::string &payload) +{ + return publish(mqtt::make_message(topic, payload)); +} + +int client_impl::publish(mqtt::const_message_ptr msg) +{ + try { + _paho_client->publish(msg)->wait_for(std::chrono::milliseconds(msg_timeout_default_ms)); + } catch (const mqtt::exception &exc) { + spdlog::error("Client {} failed to publish", _id); + return -1; + } + spdlog::trace("published message {} to {}", msg->get_payload_str(), msg->get_topic()); + return 0; +} + +int client_impl::subscribe(const std::string &topic, const std::function &cb) +{ + std::string t; + + t = topic::regexify_topic(topic); + _listeners[t] = cb; + try { + _paho_client->subscribe(topic, 0)->wait_for(std::chrono::seconds(_conn_timeout)); + } catch (const mqtt::exception &exc) { + spdlog::error("Client {} failed to subscribe to topic: {}", _id, topic); + _listeners.erase(t); + return -1; + } + + spdlog::trace("subscribed to topic: {}", topic); + return 0; +} + +int client_impl::unsubscribe(const std::string &topic) +{ + std::string t; + + try { + _paho_client->unsubscribe(topic)->wait_for(std::chrono::seconds(_conn_timeout)); + } catch (const mqtt::exception &exc) { + spdlog::error("Client {} failed to unsubscribe from topic: {}", _id, topic); + return -1; + } + spdlog::trace("unsubscribed from topic: {}", topic); + t = topic::regexify_topic(topic); + for (auto it = _listeners.begin(); it != _listeners.end();) { + if (topic::topic_matches(it->first, t)) { + it = _listeners.erase(it); + } else + ++it; + } + return 0; +} + +void client_impl::message_arrived(mqtt::const_message_ptr msg) +{ + spdlog::trace("message arrived on topic: {}", msg->get_topic()); + + if (_listeners.count(msg->get_topic()) > 0) { + _listeners[msg->get_topic()](msg); + return; + } + + for (auto &it : _listeners) { + if (topic::topic_matches(msg->get_topic(), it.first)) { + it.second(msg); + } + } +} + +int client_impl::scan_platforms(unsigned int timeout_ms) +{ + scanner scanner(this); + + _platforms_scanned.clear(); + + auto on_platform_info = [&](mqtt::const_message_ptr msg) { + const std::string &payload = msg->get_payload_str(); + const std::string &topic = msg->get_topic(); + std::string type; + unsigned int val = 0; + json_attribute json("info"); + + if (json.parse(payload) < 0) { + spdlog::error("failed to parse platform info: {}", payload); + return; + } + + if (json.get_string("type", type) < 0) { + spdlog::error("failed to parse type info: {}", payload); + return; + } + if (type != "platform" || _platforms_scanned.find(topic) != _platforms_scanned.end()) + return; + + _platforms_scanned.insert(topic); + + spdlog::trace("received platform info: {}", payload); + + if (json.get_unsigned_int("number_of_devices", val) < 0) { + spdlog::error("failed to parse platform info: {}", payload); + return; + } + _device_count += val; + }; + + scanner.set_scan_timeout_ms(timeout_ms) + .set_message_callback(on_platform_info) + .set_condition_callback([&]() { + std::this_thread::sleep_for(std::chrono::milliseconds(platforms_timeout_ms)); + return true; + }) + .set_publisher(mqtt::make_message("pza", "p")) + .set_subscription_topic("pza/server/+/+/atts/info") + .run(); + + if (_platforms_scanned.empty()) { + spdlog::error("No platforms found"); + return -1; + } + + if (_device_count == 0) { + spdlog::error("No devices found on scanned platforms"); + return -1; + } + spdlog::info("Found {} platform{} with {} device{}", _platforms_scanned.size(), + _platforms_scanned.size() > 1 ? "s" : "", _device_count, _device_count > 1 ? "s" : ""); + return 0; +} + +int client_impl::scan_devices(unsigned int timeout_ms) +{ + scanner scanner(this); + + _devices_scanned.clear(); + + auto on_device_info = [&](mqtt::const_message_ptr msg) { + std::string base_topic = msg->get_topic().substr(4, msg->get_topic().find("/device/atts/info") - 4); + + spdlog::trace("received device info: {} {}", msg->get_topic(), msg->get_payload_str()); + _devices_scanned.emplace(base_topic, msg->get_payload_str()); + }; + + scanner.set_scan_timeout_ms(timeout_ms) + .set_message_callback(on_device_info) + .set_condition_callback([&]() { return (_device_count == _devices_scanned.size()); }) + .set_publisher(mqtt::make_message("pza", "d")) + .set_subscription_topic("pza/+/+/device/atts/info"); + + if (scanner.run() < 0) { + spdlog::error("timed out waiting for devices"); + return -1; + } + return 0; +} + +int client_impl::scan_device(const std::string &group, const std::string &name, unsigned int timeout_ms) +{ + scanner scanner(this); + auto combined = group + "/" + name; + + _devices.erase(combined); + + scanner.set_scan_timeout_ms(timeout_ms) + .set_message_callback(on_device_info) + .set_condition_callback([&]() { return _devices_scanned.count(combined) > 0; }) + .set_publisher(mqtt::make_message("pza", combined)) + .set_subscription_topic("pza/" + combined + "/device/atts/info"); + + if (scanner.run() < 0) { + spdlog::error("timed out waiting for device {}", combined); + return -1; + } + return 0; +} + +device::s_ptr client_impl::create_device(const std::string &group, const std::string &name) +{ + device::s_ptr dev; + json_attribute json("info"); + device_info info{}; + auto combined = group + "/" + name; + + auto elem = _devices_scanned.at(combined); + + if (json.parse(elem) < 0) { + spdlog::error("failed to parse device info for device {}", combined); + return nullptr; + } + + if (json.get_unsigned_int("number_of_interfaces", info.number_of_interfaces) < 0) { + spdlog::error("failed to parse number of interfaces for device {}", combined); + return nullptr; + } + + info.group = group; + info.name = name; + + try { + dev = std::make_shared(this, info); + } catch (const std::exception &exc) { + spdlog::error("failed to create device: {}", exc.what()); + return nullptr; + } + _devices[combined] = dev; + return dev; +} + +device::s_ptr client_impl::register_device(const std::string &group, const std::string &name, unsigned int timeout_ms) +{ + if (scan_device(group, name, timeout_ms) < 0) { + spdlog::error("failed to scan device {}", name); + return nullptr; + } + return create_device(group, name); +} + +int client_impl::register_devices(unsigned int timeout_ms) +{ + int ret = 0; + device_info info; + + if (scan_platforms(timeout_ms) < 0) { + spdlog::error("failed to scan platforms."); + return -1; + } + if (scan_devices(timeout_ms) < 0) { + spdlog::error("failed to scan devices."); + return -1; + } + for (auto const &elem : _devices_scanned) { + auto group = elem.first.substr(0, elem.first.find('/')); + auto name = elem.first.substr(elem.first.find('/') + 1); + if (create_device(group, name) == nullptr) { + spdlog::error("failed to create device {}", elem.first); + ret = -1; + } + } + return ret; +} + +device::s_ptr client_impl::get_device(const std::string &group, const std::string &name) const +{ + auto combined = group + "/" + name; + + if (_devices.count(combined) == 0) + return nullptr; + return _devices.at(combined); +} + +std::vector client_impl::get_devices() const +{ + std::vector ret; + + for (auto const &elem : _devices) { + ret.push_back(elem.second); + } + return ret; +} + +std::vector client_impl::get_devices_in_group(const std::string &group) const +{ + std::vector ret; + + for (auto const &elem : _devices) { + if (elem.second->get_group() == group) + ret.push_back(elem.second); + } + return ret; +} + +std::set client_impl::get_groups() const +{ + std::set groups; + + for (auto const &elem : _devices) { + groups.insert(elem.second->get_group()); + } + return groups; +} + +client::client(const std::string &addr, int port, std::optional id) + : _impl(std::make_unique(addr, port, id)) +{ +} + +client::~client() = default; + +int client::connect() +{ + return _impl->connect(); +} + +int client::disconnect() +{ + return _impl->disconnect(); +} + +bool client::is_connected() const +{ + return _impl->is_connected(); +} + +const std::string &client::get_addr() const +{ + return _impl->get_addr(); +} + +const std::string &client::get_id() const +{ + return _impl->get_id(); +} + +int client::get_port() const +{ + return _impl->get_port(); +} + +void client::set_connection_timeout(unsigned int timeout) +{ + _impl->set_connection_timeout(timeout); +} + +unsigned int client::get_connection_timeout() const +{ + return _impl->get_connection_timeout(); +} + +device::s_ptr client::register_device(const std::string &group, const std::string &name, unsigned int timeout_ms) +{ + return _impl->register_device(group, name, timeout_ms); +} + +int client::register_devices(unsigned int timeout_ms) +{ + return _impl->register_devices(timeout_ms); +} + +device::s_ptr client::get_device(const std::string &group, const std::string &name) const +{ + return _impl->get_device(group, name); +} + +std::vector client::get_devices() const +{ + return _impl->get_devices(); +} + +std::vector client::get_devices_in_group(const std::string &group) const +{ + return _impl->get_devices_in_group(group); +} + +std::set client::get_groups() const +{ + return _impl->get_groups(); +} + +itf_base::s_ptr client::get_interface(const std::string &group, const std::string &name, + const std::string &interface_group, unsigned int idx, + const std::string &interface_name) const +{ + auto dev = _impl->get_device(group, name); + if (dev == nullptr) + return nullptr; + return dev->get_interface(interface_group, idx, interface_name); +} + +itf_base::s_ptr client::get_interface(const std::string &group, const std::string &name, + const std::string &interface_name) const +{ + auto dev = _impl->get_device(group, name); + if (dev == nullptr) + return nullptr; + return dev->get_interface(interface_name); +} + +std::vector client::get_interfaces() const +{ + auto dev = _impl->get_devices(); + std::vector ret; + + for (auto const &elem : dev) { + auto itf = elem->get_interfaces(); + ret.insert(ret.end(), itf.begin(), itf.end()); + } + + return ret; +} diff --git a/source/core/core.cxx b/source/core/core.cxx new file mode 100644 index 0000000..73a54fe --- /dev/null +++ b/source/core/core.cxx @@ -0,0 +1,11 @@ +#include + +void pza::core::set_log_level(pza::core::log_level level) +{ + spdlog::set_level(static_cast(level)); +} + +pza::core::log_level pza::core::get_log_level() +{ + return static_cast(spdlog::get_level()); +} diff --git a/source/core/device.cxx b/source/core/device.cxx new file mode 100644 index 0000000..442b1f7 --- /dev/null +++ b/source/core/device.cxx @@ -0,0 +1,295 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include "../utils/json_attribute.hxx" +#include "interface_factory.hxx" +#include "mqtt_service.hxx" +#include "scanner.hxx" + +using namespace pza; + +struct device_info { + std::string name; + std::string group; + std::string model; + std::string manufacturer; + std::string family; + unsigned int number_of_interfaces; +}; + +static constexpr unsigned int interfaces_timeout = 1000; + +struct device_impl { + explicit device_impl(mqtt_service *mqtt, const struct device_info &info); + device_impl(const device_impl &) = delete; + device_impl(device_impl &&) = delete; + device_impl &operator=(const device_impl &) = delete; + device_impl &operator=(device_impl &&) = delete; + ~device_impl(); + + void on_interface_info(mqtt::const_message_ptr msg); + + unsigned int get_number_of_interfaces() const + { + return interfaces.size(); + } + + itf_base::s_ptr find_interface(const std::string &name) const; + itf_base::s_ptr get_interface(const std::string &name) const; + itf_base::s_ptr get_interface(const std::string &interface_group, unsigned int idx, + const std::string &name) const; + + std::vector get_interfaces_name() const; + + std::vector get_interfaces_in_group(const std::string &group) const; + std::vector get_interfaces_in_group(const std::string &group, unsigned int index) const; + std::set get_interface_groups() const; + + size_t get_interface_groups_count() const; + + std::vector get_interfaces() const; + + struct device_info info; + std::unordered_map interfaces_scanned; + itf::device::s_ptr device_interface = nullptr; + + std::map interfaces; +}; + +device_impl::device_impl(mqtt_service *mqtt, const struct device_info &info) + : info(info) +{ + scanner scanner(mqtt); + + auto on_interface_info = [&](mqtt::const_message_ptr msg) { + std::string base_topic = msg->get_topic().substr(0, msg->get_topic().find("/atts/info")); + std::string itf_name = base_topic.substr(base_topic.find_last_of('/') + 1); + + spdlog::trace("received interface info: {} {}", msg->get_topic(), msg->get_payload_str()); + + interfaces_scanned[itf_name] = msg->get_payload_str(); + }; + + scanner.set_scan_timeout_ms(interfaces_timeout) + .set_message_callback(on_interface_info) + .set_condition_callback([&]() { + return (info.number_of_interfaces && (info.number_of_interfaces == interfaces_scanned.size())); + }) + .set_publisher(mqtt::make_message("pza", info.group + "/" + info.name)) + .set_subscription_topic("pza/" + info.group + "/" + info.name + "/+/atts/info"); + + if (scanner.run() < 0) + spdlog::error("timed out waiting for interfaces, expected {} got {}", info.number_of_interfaces, + interfaces_scanned.size()); +} + +device_impl::~device_impl() = default; + +itf_base::s_ptr device_impl::find_interface(const std::string &name) const +{ + auto it = interfaces.find(name); + + if (it == interfaces.end()) { + spdlog::error("interface {} not found", name); + return nullptr; + } + return it->second; +} + +itf_base::s_ptr device_impl::get_interface(const std::string &name) const +{ + return find_interface(name); +} + +itf_base::s_ptr device_impl::get_interface(const std::string &interface_group, unsigned int idx, + const std::string &name) const +{ + return find_interface(utils::format_interface_group(interface_group, idx, name)); +} + +std::vector device_impl::get_interfaces_name() const +{ + std::vector names; + + for (auto &it : interfaces) { + names.push_back(it.first); + } + return names; +} + +std::vector device_impl::get_interfaces_in_group(const std::string &group) const +{ + std::vector interfaces_in_group; + struct utils::itf_group_info info; + + for (const auto &itf : interfaces) { + if (utils::get_grouped_interface_info(itf.first, info) == 0 && info.group_name == group) { + interfaces_in_group.push_back(itf.second); + } + } + return interfaces_in_group; +} + +std::vector device_impl::get_interfaces_in_group(const std::string &group, unsigned int index) const +{ + std::vector interfaces_in_group; + struct utils::itf_group_info info; + + for (const auto &itf : interfaces) { + if (utils::get_grouped_interface_info(itf.first, info) == 0 && info.group_name == group && + info.index == index) { + interfaces_in_group.push_back(itf.second); + } + } + return interfaces_in_group; +} + +std::set device_impl::get_interface_groups() const +{ + std::set unique_groups; + struct utils::itf_group_info info; + + for (const auto &itf : interfaces) { + if (utils::get_grouped_interface_info(itf.first, info) == 0) { + unique_groups.insert(info.group_name); + } + } + return unique_groups; +} + +std::vector device_impl::get_interfaces() const +{ + std::vector vec; + + for (const auto &itf : interfaces) { + vec.push_back(itf.second); + } + return vec; +} + +size_t device_impl::get_interface_groups_count() const +{ + return get_interface_groups().size(); +} + +device::device(mqtt_service *mqtt, struct device_info &info) + : _impl(std::make_unique(mqtt, info)) +{ + json_attribute json("info"); + + for (auto &itf : _impl->interfaces_scanned) { + std::string type; + + if (json.parse(itf.second) < 0) { + spdlog::error("failed to parse attribute info for interface {}", itf.first); + continue; + } + + if (json.get_string("type", type) < 0) { + spdlog::error("failed to get type for interface {}", itf.first); + continue; + } + + auto itf_ptr = interface_factory::create_interface(mqtt, info.group, info.name, itf.first, type); + if (itf_ptr == nullptr) { + spdlog::error("failed to create interface {} of type {}", itf.first, type); + continue; + } + _impl->interfaces[itf.first] = itf_ptr; + } + + if (_impl->interfaces.find("device") != _impl->interfaces.end()) { + _impl->device_interface = std::static_pointer_cast(_impl->interfaces["device"]); + _impl->info.family = _impl->device_interface->get_family(); + _impl->info.manufacturer = _impl->device_interface->get_manufacturer(); + _impl->info.model = _impl->device_interface->get_model(); + } else { + throw std::runtime_error("device interface not found"); + } +} + +device::~device() = default; + +const std::string &device::get_name() const +{ + return _impl->info.name; +} + +const std::string &device::get_group() const +{ + return _impl->info.group; +} + +const std::string &device::get_model() const +{ + return _impl->info.model; +} + +const std::string &device::get_manufacturer() const +{ + return _impl->info.manufacturer; +} + +const std::string &device::get_family() const +{ + return _impl->info.family; +} + +unsigned int device::get_number_of_interfaces() const +{ + return _impl->get_number_of_interfaces(); +} + +itf_base::s_ptr device::get_interface(const std::string &name) const +{ + return _impl->get_interface(name); +} + +itf_base::s_ptr device::get_interface(const std::string &interface_group, unsigned int idx, + const std::string &name) const +{ + return _impl->get_interface(interface_group, idx, name); +} + +std::vector device::get_interfaces_name() const +{ + return _impl->get_interfaces_name(); +} + +std::vector device::get_interfaces_in_group(const std::string &group) const +{ + return _impl->get_interfaces_in_group(group); +} + +std::vector device::get_interfaces_in_group(const std::string &group, unsigned int index) const +{ + return _impl->get_interfaces_in_group(group, index); +} + +std::set device::get_interface_groups() const +{ + return _impl->get_interface_groups(); +} + +std::vector device::get_interfaces() const +{ + return _impl->get_interfaces(); +} + +size_t device::get_interface_groups_count() const +{ + return _impl->get_interface_groups_count(); +} diff --git a/source/core/interface.cxx b/source/core/interface.cxx new file mode 100644 index 0000000..722f165 --- /dev/null +++ b/source/core/interface.cxx @@ -0,0 +1,110 @@ +#include +#include + +#include + +#include +#include + +#include "attribute.hxx" +#include "interface.hxx" +#include "mqtt_service.hxx" + +using namespace pza; + +static constexpr unsigned int attributes_timeout = 2; + +itf_impl::itf_impl(mqtt_service *mqtt, itf_info &info) + : info(info), + topic_base("pza/" + info.bench + "/" + info.device_name + "/" + info.name), + topic_cmd(topic_base + "/cmds/set"), + mqtt(mqtt) +{ +} + +itf_impl::~itf_impl() +{ + for (auto const &elem : attributes) { + auto att = elem.second.get(); + std::string att_topic = topic_base + "/atts/" + att->get_name(); + + mqtt->unsubscribe(att_topic); + } +} + +void itf_impl::register_attributes(const std::list &attribute_list) +{ + std::mutex mtx; + std::unique_lock lock(mtx); + std::condition_variable cv; + unsigned int count = 0; + + for (auto const &att : attribute_list) { + const std::string &att_name = att->get_name(); + std::string att_topic = topic_base + "/atts/" + att_name; + + attributes[att_name] = att; + mqtt->subscribe(att_topic, [&](mqtt::const_message_ptr msg) { + att->on_message(msg); + count++; + cv.notify_one(); + }); + } + + if (cv.wait_for(lock, std::chrono::seconds(attributes_timeout), + [&]() { return (count == attributes.size()); }) == false) { + throw std::runtime_error("timed out waiting for attributes"); + } + + auto send_message = [&](const nlohmann::json &data) { + return mqtt->publish(mqtt::make_message(topic_cmd, data.dump())); + }; + + auto on_message = [&](mqtt::const_message_ptr msg) { + nlohmann::json data = nlohmann::json::parse(msg->get_payload_str()); + std::string att_name = data.begin().key(); + auto it = attributes.find(att_name); + + if (it == attributes.end()) { + spdlog::error("attribute not found: {}", att_name); + return; + } + + it->second->on_message(msg); + }; + + for (auto const &elem : attributes) { + auto att = elem.second; + std::string att_topic = topic_base + "/atts/" + att->get_name(); + + att->set_msg_callback(send_message); + mqtt->subscribe(att_topic, on_message); + } +} + +attribute &itf_impl::get_attribute(const std::string &name) +{ + return *attributes[name]; +} + +int itf_impl::send_message(const nlohmann::json &data) +{ + return mqtt->publish(mqtt::make_message(topic_cmd, data.dump())); +} + +itf_base::itf_base(mqtt_service *mqtt, itf_info &info) + : _impl(std::make_unique(mqtt, info)) +{ +} + +itf_base::~itf_base() = default; + +const std::string &itf_base::get_name() const +{ + return _impl->get_name(); +} + +const std::string &itf_base::get_type() const +{ + return _impl->get_type(); +} diff --git a/source/core/interface.hxx b/source/core/interface.hxx new file mode 100644 index 0000000..022963b --- /dev/null +++ b/source/core/interface.hxx @@ -0,0 +1,42 @@ +#include +#include + +#include + +#include + +#include "attribute.hxx" +#include "mqtt_service.hxx" + +using namespace pza; + +struct itf_impl { + explicit itf_impl(mqtt_service *mqtt, itf_info &info); + + virtual ~itf_impl(); + itf_impl(const itf_impl &) = delete; + itf_impl &operator=(const itf_impl &) = delete; + itf_impl(itf_impl &&) = delete; + itf_impl &operator=(itf_impl &&) = delete; + + const std::string &get_name() const + { + return info.name; + } + const std::string &get_type() const + { + return info.type; + } + + void register_attributes(const std::list &attribute_list); + + attribute &get_attribute(const std::string &name); + + int send_message(const nlohmann::json &data); + + itf_info info; + std::string topic_base; + std::string topic_cmd; + mqtt_service *mqtt; + std::unordered_map attributes; +}; diff --git a/source/core/interface_factory.cxx b/source/core/interface_factory.cxx new file mode 100644 index 0000000..983f60b --- /dev/null +++ b/source/core/interface_factory.cxx @@ -0,0 +1,41 @@ +#include "interface_factory.hxx" + +#include + +#include +#include +#include +#include +#include + +template static itf_base::s_ptr allocate_interface(mqtt_service *mqtt, itf_info &info) +{ + return std::make_shared(mqtt, info); +} + +static const std::unordered_map factory_map = { + {"device", allocate_interface}, + {"platform", allocate_interface}, + {"ammeter", allocate_interface}, + {"voltmeter", allocate_interface}, + {"bpc", allocate_interface}}; + +itf_base::s_ptr interface_factory::create_interface(mqtt_service *mqtt, const std::string &group, + const std::string &device_name, const std::string &name, + const std::string &type) +{ + itf_info info; + + info.name = name; + info.bench = group; + info.device_name = device_name; + info.type = type; + + auto it = factory_map.find(type); + + if (it == factory_map.end()) { + spdlog::error("Unknown interface type {}", type); + return nullptr; + } + return it->second(mqtt, info); +} diff --git a/source/core/interface_factory.hxx b/source/core/interface_factory.hxx new file mode 100644 index 0000000..ac8b1c7 --- /dev/null +++ b/source/core/interface_factory.hxx @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include + +#include +#include + +#include "interface.hxx" + +using namespace pza; + +namespace interface_factory +{ +using factory_function = std::function; + +itf_base::s_ptr create_interface(mqtt_service *mqtt, const std::string &group, const std::string &device_name, + const std::string &name, const std::string &type); +}; diff --git a/source/core/mqtt_service.hxx b/source/core/mqtt_service.hxx new file mode 100644 index 0000000..ccf6b5a --- /dev/null +++ b/source/core/mqtt_service.hxx @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include + +class mqtt_service +{ +public: + using s_ptr = std::shared_ptr; + + mqtt_service() = default; + mqtt_service(const mqtt_service &) = delete; + mqtt_service &operator=(const mqtt_service &) = delete; + mqtt_service(mqtt_service &&) = delete; + mqtt_service &operator=(mqtt_service &&) = delete; + virtual ~mqtt_service() = default; + + virtual int publish(const std::string &topic, const std::string &payload) = 0; + virtual int publish(mqtt::const_message_ptr msg) = 0; + virtual int subscribe(const std::string &topic, const std::function &cb) = 0; + virtual int unsubscribe(const std::string &topic) = 0; +}; diff --git a/source/core/scanner.cxx b/source/core/scanner.cxx new file mode 100644 index 0000000..517aee9 --- /dev/null +++ b/source/core/scanner.cxx @@ -0,0 +1,31 @@ +#include "scanner.hxx" + +scanner::scanner(mqtt_service *mqtt) + : _mqtt(mqtt), + _message_cb(nullptr), + _condition_cb(nullptr), + _pub_msg(nullptr) +{ +} + +int scanner::run() +{ + bool ret = false; + std::unique_lock lock(_mtx); + + if (_message_cb == nullptr || _condition_cb == nullptr || _pub_msg == nullptr || _sub_topic.empty()) { + spdlog::error("scanner not configured"); + return -1; + } + + auto _on_message = [&](mqtt::const_message_ptr msg) { + _message_cb(msg); + _cv.notify_one(); + }; + + _mqtt->subscribe(_sub_topic, _on_message); + _mqtt->publish(_pub_msg); + ret = _cv.wait_for(lock, std::chrono::milliseconds(_scan_timeout), _condition_cb); + _mqtt->unsubscribe(_sub_topic); + return (ret == true) ? 0 : -1; +} diff --git a/source/core/scanner.hxx b/source/core/scanner.hxx new file mode 100644 index 0000000..7306bb3 --- /dev/null +++ b/source/core/scanner.hxx @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include + +#include "mqtt_service.hxx" + +class scanner +{ +public: + explicit scanner(mqtt_service *mqtt); + + scanner &set_scan_timeout_ms(unsigned int timeout) + { + _scan_timeout = timeout; + return *this; + } + scanner &set_message_callback(const std::function &cb) + { + _message_cb = cb; + return *this; + } + scanner &set_condition_callback(const std::function &cb) + { + _condition_cb = cb; + return *this; + } + scanner &set_subscription_topic(const std::string &topic) + { + _sub_topic = topic; + return *this; + } + scanner &set_publisher(mqtt::const_message_ptr msg) + { + _pub_msg = msg; + return *this; + } + int run(); + +private: + static constexpr unsigned int _scan_timeout_default = 3; // in seconds + unsigned int _scan_timeout = _scan_timeout_default; + mqtt_service *_mqtt; + std::function _message_cb; + std::function _condition_cb; + std::condition_variable _cv; + std::mutex _mtx; + std::string _sub_topic; + mqtt::const_message_ptr _pub_msg; +}; diff --git a/source/core/version.cxx.in b/source/core/version.cxx.in new file mode 100644 index 0000000..7d69032 --- /dev/null +++ b/source/core/version.cxx.in @@ -0,0 +1,29 @@ +#include + +namespace pza::core +{ + std::string get_version() + { + return "@PZACXX_VERSION@"; + } + + std::string get_version_githash() + { + return "@PZACXX_VERSION_GITHASH@"; + } + + unsigned int get_version_major() + { + return @PZACXX_VERSION_MAJOR@; + } + + unsigned int get_version_minor() + { + return @PZACXX_VERSION_MINOR@; + } + + unsigned int get_version_patch() + { + return @PZACXX_VERSION_PATCH@; + } +} diff --git a/source/interfaces/ammeter.cxx b/source/interfaces/ammeter.cxx new file mode 100644 index 0000000..5212eeb --- /dev/null +++ b/source/interfaces/ammeter.cxx @@ -0,0 +1,12 @@ +#include + +#include "../core/attribute.hxx" + +using namespace pza::itf; + +ammeter::ammeter(mqtt_service *mqtt, itf_info &info) + : meter(mqtt, info) +{ +} + +ammeter::~ammeter() = default; diff --git a/source/interfaces/bpc.cxx b/source/interfaces/bpc.cxx new file mode 100644 index 0000000..23901f0 --- /dev/null +++ b/source/interfaces/bpc.cxx @@ -0,0 +1,117 @@ +#include + +#include "../core/interface.hxx" + +using namespace pza::itf; + +bpc::bpc(mqtt_service *mqtt, itf_info &info) + : itf_base(mqtt, info) +{ + _enable = std::make_shared("enable"); + _voltage = std::make_shared("voltage"); + _current = std::make_shared("current"); + + _enable->register_field("value"); + + _voltage->register_field("value"); + _voltage->register_field("min"); + _voltage->register_field("max"); + _voltage->register_field("decimals"); + + _current->register_field("value"); + _current->register_field("min"); + _current->register_field("max"); + _current->register_field("decimals"); + + _impl->register_attributes({_enable, _voltage, _current}); +} + +bpc::~bpc() = default; + +int bpc::set_voltage(double volts) +{ + auto min = _voltage->get_field("min"); + auto max = _voltage->get_field("max"); + + if (volts < min || volts > max) { + spdlog::error("bpc::set_voltage: voltage out of " + "range, min: {}, max: {}, value: {}", + min, max, volts); + return -1; + } + return _voltage->set_field("value", volts); +} + +int bpc::set_current(double amps) +{ + auto min = _current->get_field("min"); + auto max = _current->get_field("max"); + + if (amps < min || amps > max) { + spdlog::error("bpc::set_current: current out of " + "range, min: {}, max: {}, value: {}", + min, max, amps); + return -1; + } + return _current->set_field("value", amps); +} + +int bpc::set_enable(bool enable) +{ + return _enable->set_field("value", enable); +} + +bool bpc::get_enable() +{ + return _enable->get_field("value"); +} + +double bpc::get_min_voltage() +{ + return _voltage->get_field("min"); +} + +double bpc::get_max_voltage() +{ + return _voltage->get_field("max"); +} + +double bpc::get_min_current() +{ + return _current->get_field("min"); +} + +double bpc::get_max_current() +{ + return _current->get_field("max"); +} + +double bpc::get_preset_voltage() +{ + return _voltage->get_field("value"); +} + +double bpc::get_preset_current() +{ + return _current->get_field("value"); +} + +unsigned int bpc::get_num_decimals_voltage() +{ + return _voltage->get_field("decimals"); +} + +unsigned int bpc::get_num_decimals_current() +{ + return _current->get_field("decimals"); +} + +void bpc::register_enable_callback(const std::function &callback) +{ + _enable->register_callback(callback); +} + +void bpc::remove_enable_callback(const std::function &callback) +{ + _enable->remove_callback(callback); +} diff --git a/source/interfaces/device.cxx b/source/interfaces/device.cxx new file mode 100644 index 0000000..4510a7d --- /dev/null +++ b/source/interfaces/device.cxx @@ -0,0 +1,34 @@ +#include + +#include "../core/interface.hxx" + +using namespace pza::itf; + +device::device(mqtt_service *mqtt, itf_info &info) + : itf_base(mqtt, info) +{ + _identity = std::make_shared("identity"); + + _identity->register_field("model"); + _identity->register_field("manufacturer"); + _identity->register_field("family"); + + _impl->register_attributes({_identity}); +} + +device::~device() = default; + +const std::string &device::get_model() const +{ + return _identity->get_field("model"); +} + +const std::string &device::get_manufacturer() const +{ + return _identity->get_field("manufacturer"); +} + +const std::string &device::get_family() const +{ + return _identity->get_field("family"); +} diff --git a/source/interfaces/meter.cxx b/source/interfaces/meter.cxx new file mode 100644 index 0000000..27cb19b --- /dev/null +++ b/source/interfaces/meter.cxx @@ -0,0 +1,32 @@ +#include + +#include "../core/interface.hxx" + +using namespace pza::itf; + +meter::meter(mqtt_service *mqtt, itf_info &info) + : itf_base(mqtt, info) +{ + _measure = std::make_shared("measure"); + + _measure->register_field("value"); + + _impl->register_attributes({_measure}); +} + +meter::~meter() = default; + +double meter::get_measure() +{ + return _impl->get_attribute("measure").get_field("value"); +} + +void meter::register_measure_callback(const std::function &callback) +{ + _impl->get_attribute("measure").register_callback(callback); +} + +void meter::remove_measure_callback(const std::function &callback) +{ + _impl->get_attribute("measure").remove_callback(callback); +} diff --git a/source/interfaces/platform.cxx b/source/interfaces/platform.cxx new file mode 100644 index 0000000..4dff8a3 --- /dev/null +++ b/source/interfaces/platform.cxx @@ -0,0 +1,12 @@ +#include + +#include "../core/attribute.hxx" + +using namespace pza::itf; + +platform::platform(mqtt_service *mqtt, itf_info &info) + : itf_base(mqtt, info) +{ +} + +platform::~platform() = default; diff --git a/source/interfaces/voltmeter.cxx b/source/interfaces/voltmeter.cxx new file mode 100644 index 0000000..f5c4141 --- /dev/null +++ b/source/interfaces/voltmeter.cxx @@ -0,0 +1,12 @@ +#include + +#include "../core/attribute.hxx" + +using namespace pza::itf; + +voltmeter::voltmeter(mqtt_service *mqtt, itf_info &info) + : meter(mqtt, info) +{ +} + +voltmeter::~voltmeter() = default; diff --git a/source/pza/core/attribute.cxx b/source/pza/core/attribute.cxx deleted file mode 100644 index 466c78b..0000000 --- a/source/pza/core/attribute.cxx +++ /dev/null @@ -1,90 +0,0 @@ -#include "attribute.hxx" - -#include - -using namespace pza; - -attribute::attribute(const std::string &name) - : _name(name) -{ -} - -void attribute::on_message(const mqtt::const_message_ptr &message) -{ - const std::string &payload = message->get_payload_str(); - auto json = nlohmann::json::parse(payload); - json = json[_name]; - - for (auto it = json.begin(); it != json.end(); ++it) { - spdlog::trace("Attribute {:s} received data for field {:s} with value {:s}", _name, it.key(), it.value().dump()); - - auto data = it.value(); - auto elem = _fields.find(it.key()); - - if (elem == _fields.end()) { - continue; - } - - auto &f = elem->second; - const auto &type = f.type(); - - if (type == typeid(field)) { - _assign_value(f, data); - } - else if (type == typeid(field)) { - _assign_value(f, data); - } - else if (type == typeid(field)) { - _assign_value(f, data); - } - else if (type == typeid(field)) { - _assign_value(f, data); - } - else { - spdlog::warn("Type mismatch for attribute {:s}, field {:s}.. ", _name, it.key()); - return ; - } - _waiting_for_response = false; - _cv.notify_all(); - } -} - -bool attribute::type_is_compatible(const nlohmann::json::value_t &value1, const nlohmann::json::value_t &value2) -{ - constexpr auto INTEGER = nlohmann::json::value_t::number_integer; - constexpr auto UNSIGNED = nlohmann::json::value_t::number_unsigned; - constexpr auto FLOAT = nlohmann::json::value_t::number_float; - - auto isNumber = [](const nlohmann::json::value_t &value) - { - return value == INTEGER || value == UNSIGNED || value == FLOAT; - }; - - return (value1 == value2) || (isNumber(value1) && isNumber(value2)); -} - -int attribute::data_from_field(const nlohmann::json &data) -{ - nlohmann::json json; - int ret = -1; - - json[_name] = data; - - if (_callback) { - spdlog::trace("Calling callback for attribute {:s}", _name); - _callback(json); - } - - std::unique_lock lock(_mtx); - - for (int i = 0; i < SET_TIMEOUT_RETRIES; i++) - { - if (_cv.wait_for(lock, std::chrono::seconds(SET_TIMEOUT), [&]() {return !_waiting_for_response; }) == true) { - ret = 0; - break; - } - else - spdlog::warn("Timeout while waiting for response from attribute {:s}, retrying...", _name); - } - return ret; -} \ No newline at end of file diff --git a/source/pza/core/attribute.hxx b/source/pza/core/attribute.hxx deleted file mode 100644 index dbec3e4..0000000 --- a/source/pza/core/attribute.hxx +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include - -namespace pza -{ - class attribute - { - public: - friend class itface; - - explicit attribute(const std::string &name); - - template - void add_ro_field(const std::string &name) - { - add_field(name, access_mode::readonly); - } - - template - void add_rw_field(const std::string &name) - { - add_field(name, access_mode::readwrite); - } - - void on_message(const mqtt::const_message_ptr &message); - - static bool type_is_compatible(const nlohmann::json::value_t &value1, const nlohmann::json::value_t &value2); - - template - field &get_field(const std::string &name) - { - return std::any_cast&>(_fields[name]); - } - - private: - - static constexpr int SET_TIMEOUT = 1; // in seconds - static constexpr int SET_TIMEOUT_RETRIES = 3; - - int data_from_field(const nlohmann::json &data); - - template - void add_field(const std::string &name, access_mode mode) - { - field field(name, mode); - - field._callback = std::bind(&attribute::data_from_field, this, std::placeholders::_1); - _fields[name] = field; - } - - template - void _assign_value(std::any &elem, const nlohmann::json &data) - { - try { - auto &f = std::any_cast&>(elem); - if (type_is_compatible(data.type(), f.get_json_type()) == true) - f._set_value(data.get()); - else - spdlog::error("Type mismatch for attribute {:s}, field {:s}.. ", _name, f.name()); - } - catch (const std::bad_any_cast &e) { - spdlog::error("Bad any cast for attribute {:s} : {}", _name, e.what()); - } - } - - std::map _fields; - std::string _name; - std::condition_variable _cv; - std::mutex _mtx; - bool _waiting_for_response = false; - std::function _callback = nullptr; - }; -}; \ No newline at end of file diff --git a/source/pza/core/client.cxx b/source/pza/core/client.cxx deleted file mode 100644 index 4ddb4de..0000000 --- a/source/pza/core/client.cxx +++ /dev/null @@ -1,463 +0,0 @@ -#include "client.hxx" - -using namespace pza; - -static constexpr unsigned int CONN_TIMEOUT = 5; // in seconds - -client::client(const std::string &addr, int port, const std::string &id) - : _addr(addr), - _port(port), - _id(id) -{ - std::string url = "tcp://" + addr + ":" + std::to_string(port); - - if (id.empty()) { - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution<> dis(0, 1000000); - _id = "pza_" + std::to_string(dis(gen)); - } - else - _id = id; - _paho_client = std::make_unique(url, id); - _paho_client->set_callback(*this); - spdlog::trace("created client with id: {}", _id); -} - -int client::connect(void) -{ - int ret; - - spdlog::debug("Attempting connection to {}...", _addr); - - mqtt::connect_options connOpts; - - connOpts.set_keep_alive_interval(20); - connOpts.set_clean_session(true); - _paho_client->set_callback(*this); - - try { - _paho_client->connect(connOpts)->wait_for(std::chrono::seconds(CONN_TIMEOUT)); - } - catch (const mqtt::exception &exc) { - spdlog::error("failed to connect to client: {}", exc.what()); - return -1; - } - ret = scan(); - if (ret == 0) - spdlog::info("connected to {}", _addr); - else { - disconnect(); - spdlog::error("failed to connect to {}", _addr); - } - return ret; -} - -int client::disconnect(void) -{ - spdlog::debug("Attempting to disconnect from {}...", _addr); - - try { - _paho_client->disconnect()->wait_for(std::chrono::seconds(CONN_TIMEOUT)); - } - catch (const mqtt::exception &exc) { - spdlog::error("failed to disconnect: {}", exc.what()); - return -1; - } - spdlog::info("disconnected from {}", _addr); - return 0; -} - -void client::connection_lost(const std::string &cause) -{ - spdlog::error("connection lost: {}", cause); -} - -int client::scan() -{ - if (_scan_platforms() == -1) - return -1; - if (_scan_devices() == -1) - return -1; - return 0; -} - -int client::_publish(const std::string &topic, const std::string &payload) -{ - mqtt::message_ptr pubmsg; - - pubmsg = mqtt::make_message(topic, payload); - - try { - _paho_client->publish(pubmsg)->wait(); - } - catch (const mqtt::exception &exc) { - spdlog::error("failed to publish: {}", exc.what()); - return -1; - } - spdlog::trace("published message {} to {}", payload, topic); - return 0; -} - -std::string client::_regexify_topic(const std::string &topic) -{ - std::string t = topic; - - std::replace(t.begin(), t.end(), '+', '*'); - std::replace(t.begin(), t.end(), '#', '*'); - - return t; -} - -std::string client::_convertPattern(const std::string &fnmatchPattern) { - std::string regexPattern; - for(auto& ch : fnmatchPattern){ - if(ch == '*') regexPattern += ".*"; - else if(ch == '/') regexPattern += "\\/"; - else regexPattern += ch; - } - regexPattern = "^" + regexPattern + "$"; // match the whole string - return regexPattern; -} - -bool client::_topic_matches(const std::string &str, const std::string &fnmatchPattern) { - std::string regexPattern = _convertPattern(fnmatchPattern); - std::regex pattern(regexPattern); - return std::regex_match(str, pattern); -} - -int client::_subscribe(const std::string &topic, const std::function &cb) -{ - std::string t; - - t = _regexify_topic(topic); - _listeners.emplace(t, cb); - - try { - _paho_client->subscribe(topic, 0)->wait(); - } - catch (const mqtt::exception &exc) { - spdlog::error("failed to subscribe: {}", exc.what()); - _listeners.erase(t); - return -1; - } - - spdlog::trace("subscribed to topic: {}", topic); - return 0; -} - -int client::_unsubscribe(const std::string &topic) -{ - std::string t; - - try { - _paho_client->unsubscribe(topic)->wait(); - } - catch (const mqtt::exception &exc) { - spdlog::error("failed to unsubscribe: {}", exc.what()); - return -1; - } - spdlog::trace("unsubscribed from topic: {}", topic); - t = _regexify_topic(topic); - for (auto it = _listeners.begin(); it != _listeners.end(); ) { - if (_topic_matches(it->first, t)) { - it = _listeners.erase(it); - } - else - ++it; - } - return 0; -} - -void client::message_arrived(mqtt::const_message_ptr msg) -{ - spdlog::trace("message arrived on topic: {}", msg->get_topic()); - - if (_listeners.count(msg->get_topic()) > 0) { - _listeners[msg->get_topic()](msg); - return; - } - - for (auto &it : _listeners) { - if (_topic_matches(msg->get_topic(), it.first)) { - it.second(msg); - } - } -} - -// ============================================================================ -// -int client::_scan_platforms(void) -{ - bool ret; - std::condition_variable cv; - std::unique_lock lock(_mtx); - - // Reset result variables - _scan_device_count_expected = 0; - _scan_device_results.clear(); - - // Log - spdlog::debug("scanning for platforms on {}...", _addr); - - // Prepare platform scan message processing - _subscribe("pza/server/+/+/atts/info", [&](const mqtt::const_message_ptr &msg) { - // Prepare data - std::string payload = msg->get_payload_str(); - std::string topic = msg->get_topic(); - std::string type; - unsigned int val; - - // Exclude other messages than platform - if (pza::json::get_string(payload, "info", "type", type) == -1) { - spdlog::error("failed to parse type info: {}", payload); - return; - } - - // ignore machinese - if (type != "platform") - return; - - spdlog::debug("received platform info: {}", payload); - - // @TODO HERE we should also check that we did not get the same platform twice (in the case 2 scan is performed the same time) - - // Get the number of devices - if (pza::json::get_unsigned_int(payload, "info", "number_of_devices", val) == -1) { - spdlog::error("failed to parse platform info: {}", payload); - return; - } - _scan_device_count_expected += val; - cv.notify_all(); - }); - - // Request scan for platforms and just wait for answers - _publish("pza", "p"); - ret = cv.wait_for(lock, std::chrono::seconds(_scan_timeout), [&](void) { - return (_scan_device_count_expected); - }); - _unsubscribe("pza/server/+/+/atts/info"); - - if (ret == false) { - spdlog::error("Platform scan timed out"); - return -1; - } - return 0; -} - -// ============================================================================ -// -int client::_scan_devices(void) -{ - bool ret; - std::condition_variable cv; - std::unique_lock lock(_mtx); - - // Log - spdlog::debug("scanning for devices on {}...", _addr); - - // Prepare device scan message processing - _subscribe("pza/+/+/device/atts/info", [&](const mqtt::const_message_ptr &msg) { - std::string base_topic = msg->get_topic().substr(0, msg->get_topic().find("/device/atts/info")); - spdlog::debug("received device info: {} {}", msg->get_topic(), msg->get_payload_str()); - _scan_device_results.emplace(base_topic, msg->get_payload_str()); - cv.notify_all(); - }); - - // Request for device scan - _publish("pza", "d"); - ret = cv.wait_for(lock, std::chrono::seconds(_scan_timeout), [&](void) { - return (_scan_device_count_expected && (_scan_device_count_expected == _scan_device_results.size())); - }); - _unsubscribe("pza/+/+/device/atts/info"); - - // Process timeout error - if (ret == false) { - spdlog::error("Device scan timed out"); - spdlog::debug("Expected {} devices, got {}", _scan_device_count_expected, _scan_device_results.size()); - return -1; - } - - // Process success logs - spdlog::debug("scan successful, found {} devices", _scan_device_results.size()); - if (core::get_log_level() == core::log_level::trace) { - for (auto &it : _scan_device_results) { - spdlog::trace("device: {}", it.first); - } - } - return 0; -} - -// ============================================================================ -// -int client::_scan_interfaces(std::unique_lock &lock, const device::ptr &device) -{ - bool ret; - std::condition_variable cv; - std::string itf_topic = device->_get_base_topic() + "/+/atts/info"; - const std::string &scan_payload = _scan_device_results[device->_get_base_topic()]; - - if (json::get_unsigned_int(scan_payload, "info", "number_of_interfaces", _scan_itf_count_expected) == -1) { - spdlog::error("Unknown number of interfaces for device {}", device->_get_base_topic()); - return -1; - } - - _scan_itf_results.clear(); - - _subscribe(itf_topic, [&](const mqtt::const_message_ptr &msg) { - std::string base_topic = msg->get_topic().substr(0, msg->get_topic().find("/atts/info")); - spdlog::trace("received interface info: {} {}", msg->get_topic(), msg->get_payload_str()); - base_topic = base_topic.substr(base_topic.find_last_of('/') + 1); - _scan_itf_results.emplace(base_topic, msg->get_payload_str()); - cv.notify_all(); - }); - - // Trigger the scan for the given device and wait for all interface info - auto device_short_topic = device->get_group() + "/" + device->get_name(); - _publish("pza", device_short_topic); - ret = cv.wait_for(lock, std::chrono::seconds(_scan_timeout), [&](void) { - return (_scan_itf_count_expected && (_scan_itf_count_expected == _scan_itf_results.size())); - }); - - _unsubscribe(itf_topic); - - if (ret == false) { - spdlog::error("timed out waiting for scan results"); - spdlog::error("_scan_itf_count_expected = {}, got = {}", _scan_itf_count_expected, _scan_itf_results.size()); - return -1; - } - - spdlog::debug("scan successful, found {} interfaces", _scan_itf_results.size()); - - if (core::get_log_level() == core::log_level::trace) { - for (auto &it : _scan_itf_results) { - spdlog::trace("interface: {}", it.first); - } - } - return 0; -} - -int client::register_device(const device::ptr &device) -{ - bool sane = false; - bool ret; - std::condition_variable cv; - std::unique_lock lock(_mtx); - - if (device == nullptr) { - spdlog::error("Device is null"); - return -1; - } - - if (_devices.find(device->_get_base_topic()) != _devices.end()) { - spdlog::warn("Device {} is already registered", device->_get_base_topic()); - return 0; - } - - if (_scan_device_results.find(device->_get_base_topic()) == _scan_device_results.end()) { - spdlog::error("Device {} was not scanned", device->_get_base_topic()); - return -1; - } - - _subscribe(device->_get_device_topic() + "/atts/identity", [&](const mqtt::const_message_ptr &msg) { - if (device->_set_identity(msg->get_payload_str()) == 0) - sane = true; - cv.notify_all(); - }); - - ret = cv.wait_for(lock, std::chrono::seconds(_scan_timeout), [&](void) { return (sane); }); - if (ret == false) { - spdlog::error("Device is not sane, that's very troubling"); - return -1; - } - - if (_scan_interfaces(lock, device) == -1) - return -1; - - device->_cli = this; - - if (device->_register_interfaces(_scan_itf_results) == -1) - return -1; - - _devices.emplace(device->_get_base_topic(), device); - return 0; -} - -device::ptr client::create_device(const std::string &topic_str) -{ - bool recv = false; - bool ret; - std::condition_variable cv; - std::unique_lock lock(_mtx); - mqtt::const_message_ptr identify_msg; - std::string family; - topic t(topic_str); - - if (t.is_valid() == false) { - spdlog::error("Invalid topic {}", topic_str); - return nullptr; - } - - _subscribe(topic_str + "/device/atts/identity", [&](const mqtt::const_message_ptr &msg) { - identify_msg = msg; - recv = true; - cv.notify_all(); - }); - - ret = cv.wait_for(lock, std::chrono::seconds(_scan_timeout), [&](void) { return (recv); }); - if (ret == false) { - spdlog::error("Device is not sane, that's very troubling"); - return nullptr; - } - - _unsubscribe(topic_str + "/device/atts/identity"); - - if (json::get_string(identify_msg->get_payload_str(), "identity", "family", family) == -1) { - spdlog::error("Failed to get family from device"); - return nullptr; - } - - device::ptr dev = device_factory::create_device(family, t.get_group(), t.get_device()); - - if (dev == nullptr) { - spdlog::error("Failed to create device"); - return nullptr; - } - - if (dev->_set_identity(identify_msg->get_payload_str()) == -1) { - spdlog::error("Failed to set identity"); - return nullptr; - } - - if (_scan_interfaces(lock, dev) == -1) - return nullptr; - - dev->_cli = this; - - if (dev->_register_interfaces(_scan_itf_results) == -1) - return nullptr; - - _devices.emplace(dev->_get_base_topic(), dev); - return dev; -} - - -int client::register_all_devices() -{ - int ret = 0; - - for (auto &it : _scan_device_results) { - if (create_device(it.first) == nullptr) - ret = -1; - } - return ret; -} - -device::ptr client::find_device(const std::string &group, const std::string &name) -{ - std::string base_topic = "pza/" + group + "/" + name; - - if (_devices.find(base_topic) == _devices.end()) - return nullptr; - return _devices[base_topic]; -} \ No newline at end of file diff --git a/source/pza/core/client.hxx b/source/pza/core/client.hxx deleted file mode 100644 index ede6017..0000000 --- a/source/pza/core/client.hxx +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -namespace pza -{ - class client : virtual public mqtt::callback - { - public: - using ptr = std::shared_ptr; - - friend class device; - friend class itface; - - explicit client(const std::string &addr, int port, const std::string &id = ""); - - int connect(void); - int disconnect(void); - int scan(void); - bool is_connected(void) const { return (_paho_client->is_connected()); } - - void set_scan_timeout(unsigned int timeout) { _scan_timeout = timeout; } - unsigned int get_scan_timeout(void) const { return _scan_timeout; } - - const std::string &get_addr(void) const { return _addr; } - const std::string &get_id(void) const { return _id; } - int get_port(void) const { return _port; } - - int register_device(const device::ptr &device); - int register_all_devices(); - - device::ptr find_device(const std::string &group, const std::string &name); - - using device_map = std::map; - - const device_map &get_devices(void) const { return _devices; } - - private: - using listener_map = std::map>; - - static constexpr unsigned int SCAN_TIMEOUT_DEFAULT = 5; - - unsigned int _scan_timeout = SCAN_TIMEOUT_DEFAULT; - std::string _addr; - int _port; - std::string _id; - mqtt::async_client::ptr_t _paho_client; - std::mutex _mtx; - listener_map _listeners; - - std::map _scan_device_results; - unsigned int _scan_device_count_expected = 0; - - std::map _scan_itf_results; - unsigned int _scan_itf_count_expected = 0; - - std::map _devices; - - void connection_lost(const std::string &cause) override; - void message_arrived(mqtt::const_message_ptr msg) override; - - int _publish(const std::string &topic, const std::string &payload); - int _subscribe(const std::string &topic, const std::function &cb); - int _unsubscribe(const std::string &topic); - - std::string _regexify_topic(const std::string &topic); - std::string _convertPattern(const std::string &fnmatchPattern); - bool _topic_matches(const std::string &str, const std::string &fnmatchPattern); - void _count_devices_to_scan(const std::string &payload); - int _scan_platforms(); - int _scan_devices(); - int _scan_interfaces(std::unique_lock &lock, const device::ptr &device); - device::ptr create_device(const std::string &topic); - }; -}; diff --git a/source/pza/core/core.cxx b/source/pza/core/core.cxx deleted file mode 100644 index 7e50537..0000000 --- a/source/pza/core/core.cxx +++ /dev/null @@ -1,18 +0,0 @@ -#include "core.hxx" - -using namespace pza; - -void core::set_log_level(log_level level) -{ - spdlog::set_level(static_cast(level)); -} - -core::log_level core::get_log_level(void) -{ - return static_cast(spdlog::get_level()); -} - -std::string core::get_version(void) -{ - return PZACXX_VERSION; -} \ No newline at end of file diff --git a/source/pza/core/core.hxx b/source/pza/core/core.hxx deleted file mode 100644 index 1324508..0000000 --- a/source/pza/core/core.hxx +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -namespace pza -{ - class core - { - public: - enum class log_level : int - { - trace = spdlog::level::trace, - debug = spdlog::level::debug, - info = spdlog::level::info, - warn = spdlog::level::warn, - err = spdlog::level::err, - critical = spdlog::level::critical, - off = spdlog::level::off - }; - - core() = delete; - ~core() = delete; - - static void set_log_level(log_level level); - static log_level get_log_level(void); - static std::string get_version(void); - }; -}; \ No newline at end of file diff --git a/source/pza/core/device.cxx b/source/pza/core/device.cxx deleted file mode 100644 index 7e8422e..0000000 --- a/source/pza/core/device.cxx +++ /dev/null @@ -1,56 +0,0 @@ -#include "device.hxx" -#include - -using namespace pza; - -device::device(const std::string &group, const std::string &name) - : _name(name), - _group(group), - _base_topic("pza/" + group + "/" + name), - _device_topic(_base_topic + "/device") -{ - -} - -void device::reset() -{ - _state = state::orphan; - _model = ""; - _manufacturer = ""; -} - - -int device::_set_identity(const std::string &payload) -{ - std::string family; - - if (json::get_string(payload, "identity", "model", _model) == -1) { - spdlog::error("Device does not have a model"); - return -1; - } - - if (json::get_string(payload, "identity", "manufacturer", _manufacturer) == -1) { - spdlog::error("Device does not have a manufacturer"); - return -1; - } - - if (json::get_string(payload, "identity", "family", family) == -1) { - spdlog::error("Device does not have a family"); - return -1; - } - - // Convert to lowercase - std::transform(family.begin(), family.end(), family.begin(), ::tolower); - - if (family != get_family()) { - spdlog::error("Device is not compatible {} != {}", family, get_family()); - return -1; - } - - return 0; -} - -void device::register_interface(itface &itface) -{ - _interfaces[itface._name] = &itface; -} \ No newline at end of file diff --git a/source/pza/core/device.hxx b/source/pza/core/device.hxx deleted file mode 100644 index 5e5c4c4..0000000 --- a/source/pza/core/device.hxx +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include - -namespace pza -{ - class client; - - class device - { - public: - using ptr = std::shared_ptr; - - friend class client; - friend class itface; - - enum class state : unsigned int - { - orphan = 0, - init, - running - }; - - const std::string &get_name() { return _name; } - const std::string &get_group() { return _group; } - const std::string &get_model() { return _model; } - const std::string &get_manufacturer() { return _manufacturer; } - client *get_client() { return _cli; } - virtual const std::string &get_family() = 0; - - void reset(); - enum state get_state() { return _state; } - void register_interface(itface &itface); - - protected: - device(const std::string &group, const std::string &name); - - virtual int _register_interfaces(const std::map &map) = 0; - int _set_identity(const std::string &payload); - const std::string &_get_base_topic() { return _base_topic; } - const std::string &_get_device_topic() { return _device_topic; } - - - client *_cli = nullptr; - - std::string _name; - std::string _group; - std::string _model = "none"; - std::string _manufacturer = "none"; - - std::string _base_topic; - std::string _device_topic; - - std::map _interfaces; - - enum state _state = state::orphan; - }; -}; diff --git a/source/pza/core/device_factory.cxx b/source/pza/core/device_factory.cxx deleted file mode 100644 index 81ce6ab..0000000 --- a/source/pza/core/device_factory.cxx +++ /dev/null @@ -1,21 +0,0 @@ -#include "device_factory.hxx" - -using namespace pza; - - -device::ptr device_factory::create_device(const std::string &family, const std::string &group, const std::string &name) -{ - static std::map factory_map = { - { "bps", device_factory::allocate_device } - }; - std::string family_lower = family; - - std::transform(family_lower.begin(), family_lower.end(), family_lower.begin(), ::tolower); - auto it = factory_map.find(family_lower); - if (it == factory_map.end()) { - spdlog::error("Unknown device type {}", family); - return nullptr; - } - - return it->second(group, name); -} \ No newline at end of file diff --git a/source/pza/core/device_factory.hxx b/source/pza/core/device_factory.hxx deleted file mode 100644 index cfd78dd..0000000 --- a/source/pza/core/device_factory.hxx +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include - -namespace pza -{ - class device_factory - { - public: - device_factory() = delete; - ~device_factory() = delete; - device_factory(const device_factory &) = delete; - device_factory &operator=(const device_factory &) = delete; - - static device::ptr create_device(const std::string &family, const std::string &group, const std::string &name); - - template - static device::ptr allocate_device(const std::string &group, const std::string &name) - { - return std::make_shared(group, name); - } - - private: - using factory_function = std::function; - }; -}; diff --git a/source/pza/core/field.hxx b/source/pza/core/field.hxx deleted file mode 100644 index c2538a9..0000000 --- a/source/pza/core/field.hxx +++ /dev/null @@ -1,124 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace pza -{ - enum class access_mode - { - readonly, - readwrite - }; - - template - class field - { - public: - friend class attribute; - - explicit field(const std::string &name, access_mode mode = access_mode::readonly) - : _value(_type()), - _name(name), - _mode(mode) - { - _setJsonType(); - } - - const std::string &name() const - { - return _name; - } - - const _type &get(void) const - { - return _value; - } - - int set(const _type &value) - { - nlohmann::json data; - - if (value == _value) - return 0; - - if (!_callback) { - spdlog::error("No callback set for field.. Make sure the interface is bound to a client."); - return -1; - } - data[this->_name] = value; - return _callback(data); - } - - bool is_readonly(void) const - { - return (_mode == access_mode::readonly); - } - - using get_callback_type = std::function; - - void add_get_callback(const get_callback_type &callback) - { - _get_callbacks.push_back(std::make_shared(callback)); - } - - void remove_get_callback(const get_callback_type &callback) - { - _get_callbacks.remove_if([&](const std::shared_ptr& ptr) { - return callback.target_type() == ptr->target_type(); - }); - } - - private: - void _setJsonType() - { - if (typeid(_type) == typeid(int)) - { - _json_type = nlohmann::json::value_t::number_integer; - } - else if (typeid(_type) == typeid(double)) - { - _json_type = nlohmann::json::value_t::number_float; - } - else if (typeid(_type) == typeid(bool)) - { - _json_type = nlohmann::json::value_t::boolean; - } - else if (typeid(_type) == typeid(std::string)) - { - _json_type = nlohmann::json::value_t::string; - } - else if (typeid(_type) == typeid(std::nullptr_t)) - { - _json_type = nlohmann::json::value_t::null; - } - else - { - throw std::runtime_error("Invalid type"); - } - } - - const nlohmann::json::value_t &get_json_type() const - { - return _json_type; - } - - void _set_value(const _type &value) - { - _value = value; - for (auto const &cb : _get_callbacks) { - (*cb)(value); - } - } - - _type _value; - std::string _name; - nlohmann::json::value_t _json_type; - std::list> _get_callbacks; - access_mode _mode; - std::function _callback; - }; -}; \ No newline at end of file diff --git a/source/pza/core/grouped_interface.hxx b/source/pza/core/grouped_interface.hxx deleted file mode 100644 index 3de5089..0000000 --- a/source/pza/core/grouped_interface.hxx +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "interface.hxx" - -#include - -#include -#include - -namespace pza -{ - class device; - - class grouped_interface - { - public: - using ptr = std::shared_ptr; - - grouped_interface() = delete; - grouped_interface(const grouped_interface&) = delete; - grouped_interface(grouped_interface&&) = delete; - ~grouped_interface() = delete; - - template - static int register_interfaces(device *device, const std::string &name, const std::map &map, std::vector> &channels) - { - int ret = 0; - size_t pos = 0; - int chan_id = -1; - - for (auto const &elem : map) { - if (pza::string::starts_with(elem.first, ":" + name + "_") == true) { - pos = elem.first.find_first_of('_') + 1; - chan_id = std::stoi(elem.first.substr(pos, elem.first.find_last_of(':') - pos)); - } - } - - if (chan_id == -1) { - spdlog::error("No {} channels found", name); - return -1; - } - - channels.reserve(chan_id + 1); - for (int i = 0; i < chan_id + 1; i++) { - channels.push_back(std::make_shared(device, ":" + name + "_" + std::to_string(i) + ":_")); - } - - return ret; - } - }; -}; \ No newline at end of file diff --git a/source/pza/core/interface.cxx b/source/pza/core/interface.cxx deleted file mode 100644 index 9a89c0b..0000000 --- a/source/pza/core/interface.cxx +++ /dev/null @@ -1,46 +0,0 @@ -#include "interface.hxx" -#include -#include - -using namespace pza; - -itface::itface(device *device, const std::string &name) - : _device(device), - _name(name) -{ - _topic_base = _device->_get_base_topic() + "/" + _name; - _topic_cmd = _topic_base + "/cmds/set"; -} - -void itface::register_attribute(attribute &attribute) -{ - std::condition_variable cv; - std::unique_lock lock(_mtx); - bool received = false; - std::string topic = _topic_base + "/atts/" + attribute._name; - - _device->get_client()->_subscribe(topic, [&](const mqtt::const_message_ptr &msg) { - attribute.on_message(msg); - attribute._callback = [&](const nlohmann::json &data) { - _device->get_client()->_publish(_topic_cmd, data.dump()); - }; - _attributes[attribute._name] = &attribute; - received = true; - cv.notify_one(); - }); - - if (cv.wait_for(lock, std::chrono::seconds(5), [&]() { return received; }) == false) { - spdlog::error("timed out waiting for attribute registration ({})", topic); - } - _device->get_client()->_unsubscribe(topic); - if (received) { - _device->get_client()->_subscribe(topic, std::bind(&attribute::on_message, &attribute, std::placeholders::_1)); - } -} - -void itface::register_attributes(const std::vector &list) -{ - for (auto const &it : list) { - register_attribute(*it); - } -} \ No newline at end of file diff --git a/source/pza/core/interface.hxx b/source/pza/core/interface.hxx deleted file mode 100644 index 655bce6..0000000 --- a/source/pza/core/interface.hxx +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace pza -{ - class device; - - class itface - { - public: - friend class device; - - itface(device *device, const std::string &name); - - void register_attribute(attribute &attribute); - void register_attributes(const std::vector &list); - - protected: - device *_device; - std::string _name; - std::string _topic_base; - std::string _topic_cmd; - - std::map _attributes; - - private: - std::mutex _mtx; - }; -}; \ No newline at end of file diff --git a/source/pza/devices/bps.cxx b/source/pza/devices/bps.cxx deleted file mode 100644 index e7bcdfb..0000000 --- a/source/pza/devices/bps.cxx +++ /dev/null @@ -1,33 +0,0 @@ -#include "bps.hxx" - -using namespace pza; - -bps_channel::bps_channel(device *device, const std::string &base_name) - : voltmeter(device, base_name + "vm"), - ampermeter(device, base_name + "am"), - ctrl(device, base_name + "ctrl") -{ - -} - -bps::bps(const std::string &group, const std::string &name) - : device(group, name) -{ - -} - -int bps::_register_interfaces(const std::map &map) -{ - int ret; - - ret = grouped_interface::register_interfaces(this, "channel", map, channel); - if (ret < 0) - return ret; - - for (auto &chan : channel) { - register_interface(chan->voltmeter); - register_interface(chan->ampermeter); - register_interface(chan->ctrl); - } - return 0; -} \ No newline at end of file diff --git a/source/pza/devices/bps.hxx b/source/pza/devices/bps.hxx deleted file mode 100644 index b108084..0000000 --- a/source/pza/devices/bps.hxx +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include - -namespace pza -{ - class bps_channel - { - public: - using ptr = std::shared_ptr; - - friend class bps; - - bps_channel(device *device, const std::string &base_name); - - meter voltmeter; - meter ampermeter; - bps_chan_ctrl ctrl; - }; - - class bps : public device - { - public: - using ptr = std::shared_ptr; - - explicit bps(const std::string &group, const std::string &name); - - const std::string &get_family() override { return _family; }; - size_t get_num_channels() { return channel.size(); } - std::vector channel; - - private: - int _register_interfaces(const std::map &map) override; - - std::string _family = "bps"; - }; -}; diff --git a/source/pza/interfaces/bps_chan_ctrl.cxx b/source/pza/interfaces/bps_chan_ctrl.cxx deleted file mode 100644 index 8811309..0000000 --- a/source/pza/interfaces/bps_chan_ctrl.cxx +++ /dev/null @@ -1,78 +0,0 @@ -#include "bps_chan_ctrl.hxx" - -#include - -using namespace pza; - -bps_chan_ctrl::bps_chan_ctrl(device *device, const std::string &name) - : itface(device, name), - _att_voltage("voltage"), - _att_current("current"), - _enable("enable") -{ - _att_voltage.add_rw_field("value"); - _att_voltage.add_ro_field("min"); - _att_voltage.add_ro_field("max"); - _att_voltage.add_ro_field("decimals"); - - _att_current.add_rw_field("value"); - _att_current.add_ro_field("min"); - _att_current.add_ro_field("max"); - _att_current.add_ro_field("decimals"); - - _enable.add_rw_field("value"); - _enable.add_rw_field("polling_cycle"); - - register_attributes({&_att_voltage, &_att_current, &_enable}); -} - -int bps_chan_ctrl::set_voltage(double volts) -{ - double min = _att_voltage.get_field("min").get(); - double max = _att_voltage.get_field("max").get(); - - if (volts < min || volts > max) { - spdlog::error("You can't set voltage to {}, range is {} to {}", volts, min, max); - return -1; - } - - return _att_voltage.get_field("value").set(volts); -} - -int bps_chan_ctrl::set_current(double amps) -{ - double min = _att_current.get_field("min").get(); - double max = _att_current.get_field("max").get(); - - if (amps < min || amps > max) { - spdlog::error("You can't set current to {}, range is {} to {}", amps, min, max); - return -1; - } - - return _att_current.get_field("value").set(amps); -} - -int bps_chan_ctrl::set_enable(bool enable) -{ - return _enable.get_field("value").set(enable); -} - -bool bps_chan_ctrl::get_enable() -{ - return _enable.get_field("value").get(); -} - -int bps_chan_ctrl::set_enable_polling_cycle(double seconds) -{ - return _enable.get_field("polling_cycle").set(seconds); -} - -void bps_chan_ctrl::add_enable_callback(const std::function &callback) -{ - _enable.get_field("value").add_get_callback(callback); -} - -void bps_chan_ctrl::remove_enable_callback(const std::function &callback) -{ - _enable.get_field("value").remove_get_callback(callback); -} diff --git a/source/pza/interfaces/bps_chan_ctrl.hxx b/source/pza/interfaces/bps_chan_ctrl.hxx deleted file mode 100644 index a1b0cde..0000000 --- a/source/pza/interfaces/bps_chan_ctrl.hxx +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include - -namespace pza -{ - class device; - - class bps_chan_ctrl : public itface - { - public: - using ptr = std::shared_ptr; - - bps_chan_ctrl(device *device, const std::string &name); - - int set_voltage(double volts); - int set_current(double amps); - int set_enable(bool enable); - int set_enable_polling_cycle(double seconds); - bool get_enable(); - - void add_enable_callback(const std::function &callback); - void remove_enable_callback(const std::function &callback); - - private: - attribute _att_voltage; - attribute _att_current; - attribute _enable; - }; -}; \ No newline at end of file diff --git a/source/pza/interfaces/meter.cxx b/source/pza/interfaces/meter.cxx deleted file mode 100644 index 1921899..0000000 --- a/source/pza/interfaces/meter.cxx +++ /dev/null @@ -1,34 +0,0 @@ -#include "meter.hxx" - -using namespace pza; - -meter::meter(device *device, const std::string &name) - : itface(device, name), - _measure("measure") -{ - _measure.add_ro_field("value"); - _measure.add_ro_field("polling_cycle"); - - - register_attributes({&_measure}); -} - -double meter::get_measure() -{ - return _measure.get_field("value").get(); -} - -int meter::set_measure_polling_cycle(double seconds) -{ - return _measure.get_field("polling_cycle").set(seconds); -} - -void meter::add_measure_callback(const std::function &callback) -{ - _measure.get_field("value").add_get_callback(callback); -} - -void meter::remove_measure_callback(const std::function &callback) -{ - _measure.get_field("value").remove_get_callback(callback); -} \ No newline at end of file diff --git a/source/pza/interfaces/meter.hxx b/source/pza/interfaces/meter.hxx deleted file mode 100644 index 6f79e15..0000000 --- a/source/pza/interfaces/meter.hxx +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include - -namespace pza { - class meter : public itface - { - public: - meter(device *device, const std::string &name); - - double get_measure(); - int set_measure_polling_cycle(double seconds); - - void add_measure_callback(const std::function &callback); - void remove_measure_callback(const std::function &callback); - - private: - attribute _measure; - }; -}; \ No newline at end of file diff --git a/source/pza/utils/json.cxx b/source/pza/utils/json.cxx deleted file mode 100644 index 1ae8137..0000000 --- a/source/pza/utils/json.cxx +++ /dev/null @@ -1,112 +0,0 @@ -#include "json.hxx" - -using namespace pza; -using namespace json; - -int json::_parse(const std::string &payload, nlohmann::json &json) -{ - try { - json = nlohmann::json::parse(payload); - } catch (nlohmann::json::parse_error &e) { - return -1; - } - return 0; -} - -int json::get_string(const std::string &payload, const std::string &atts, const std::string &key, std::string &str) -{ - nlohmann::json json; - if (_parse(payload, json) < 0) { - return -1; - } - try { - str = json[atts][key].get(); - } catch (nlohmann::json::type_error &e) { - return -1; - } - return 0; -} - -int json::get_int(const std::string &payload, const std::string &atts, const std::string &key, int &i) -{ - nlohmann::json json; - if (_parse(payload, json) < 0) { - return -1; - } - try { - i = json[atts][key].get(); - } catch (nlohmann::json::type_error &e) { - return -1; - } - return 0; -} - -int json::get_unsigned_int(const std::string &payload, const std::string &atts, const std::string &key, unsigned &u) -{ - nlohmann::json json; - if (_parse(payload, json) < 0) { - return -1; - } - try { - u = json[atts][key].get(); - } catch (nlohmann::json::type_error &e) { - return -1; - } - return 0; -} - -int json::get_double(const std::string &payload, const std::string &atts, const std::string &key, double &f) -{ - nlohmann::json json; - if (_parse(payload, json) < 0) { - return -1; - } - try { - f = json[atts][key].get(); - } catch (nlohmann::json::type_error &e) { - return -1; - } - return 0; -} - -int json::get_bool(const std::string &payload, const std::string &atts, const std::string &key, bool &b) -{ - nlohmann::json json; - if (_parse(payload, json) < 0) { - return -1; - } - try { - b = json[atts][key].get(); - } catch (nlohmann::json::type_error &e) { - return -1; - } - return 0; -} - -int json::get_array(const std::string &payload, const std::string &atts, const std::string &key, nlohmann::json &json) -{ - nlohmann::json j; - if (_parse(payload, j) < 0) { - return -1; - } - try { - json = j[atts][key]; - } catch (nlohmann::json::type_error &e) { - return -1; - } - return 0; -} - -int json::get_object(const std::string &payload, const std::string &atts, const std::string &key, nlohmann::json &json) -{ - nlohmann::json j; - if (_parse(payload, j) < 0) { - return -1; - } - try { - json = j[atts][key]; - } catch (nlohmann::json::type_error &e) { - return -1; - } - return 0; -} \ No newline at end of file diff --git a/source/pza/utils/json.hxx b/source/pza/utils/json.hxx deleted file mode 100644 index 72ecec6..0000000 --- a/source/pza/utils/json.hxx +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -namespace pza -{ - namespace json - { - int get_string(const std::string &payload, const std::string &atts, const std::string &key, std::string &str); - int get_int(const std::string &payload, const std::string &atts, const std::string &key, int &i); - int get_unsigned_int(const std::string &payload, const std::string &atts, const std::string &key, unsigned &u); - int get_double(const std::string &payload, const std::string &atts, const std::string &key, double &f); - int get_bool(const std::string &payload, const std::string &atts, const std::string &key, bool &b); - int get_array(const std::string &payload, const std::string &atts, const std::string &key, nlohmann::json &json); - int get_object(const std::string &payload, const std::string &atts, const std::string &key, nlohmann::json &json); - int _parse(const std::string &payload, nlohmann::json &json); - }; -}; diff --git a/source/pza/utils/string.cxx b/source/pza/utils/string.cxx deleted file mode 100644 index 03d0b90..0000000 --- a/source/pza/utils/string.cxx +++ /dev/null @@ -1,8 +0,0 @@ -#include "string.hxx" - -using namespace pza; - -bool string::starts_with(const std::string &s, const std::string &prefix) -{ - return (s.rfind(prefix, 0) == 0); -} \ No newline at end of file diff --git a/source/pza/utils/string.hxx b/source/pza/utils/string.hxx deleted file mode 100644 index 3482756..0000000 --- a/source/pza/utils/string.hxx +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include - -namespace pza -{ - namespace string - { - bool starts_with(const std::string &s, const std::string &prefix); - }; -}; \ No newline at end of file diff --git a/source/pza/utils/topic.cxx b/source/pza/utils/topic.cxx deleted file mode 100644 index a772753..0000000 --- a/source/pza/utils/topic.cxx +++ /dev/null @@ -1,24 +0,0 @@ -#include "topic.hxx" - -namespace pza -{ - topic::topic(const std::string &topic) - : _topic(topic), - _is_valid(false) - { - std::stringstream strs(topic); - std::string buf; - - _list.resize(3); - for (unsigned int i = 0; std::getline(strs, buf, '/') && i < 3; i++) { - _list[i] = buf; - } - if (_list[0].empty() || _list[1].empty() || _list[2].empty()) { - return ; - } - if (_list[0] != "pza") { - return ; - } - _is_valid = true; - } -}; \ No newline at end of file diff --git a/source/pza/utils/topic.hxx b/source/pza/utils/topic.hxx deleted file mode 100644 index a9735ef..0000000 --- a/source/pza/utils/topic.hxx +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace pza -{ - class topic - { - public: - explicit topic(const std::string &topic); - - bool is_valid() const { return _is_valid; } - - std::string get_topic() const { return _topic;} - std::string get_group() const { return _list[1]; } - std::string get_device() const { return _list[2]; } - - private: - std::string _topic; - bool _is_valid = false; - std::vector _list; - }; -}; \ No newline at end of file diff --git a/source/pza/version.hxx.in b/source/pza/version.hxx.in deleted file mode 100644 index c0a3695..0000000 --- a/source/pza/version.hxx.in +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#define PZACXX_VERSION "@PROJECT_VERSION@" \ No newline at end of file diff --git a/source/utils/interface_group.cxx b/source/utils/interface_group.cxx new file mode 100644 index 0000000..f43628c --- /dev/null +++ b/source/utils/interface_group.cxx @@ -0,0 +1,38 @@ +#include + +#include + +#include + +static const std::string regex_str = ":([a-zA-Z0-9]+)_([0-9]+):_([a-zA-Z0-9]+)"; + +namespace pza::utils +{ +bool is_interface_grouped(const std::string &itf_name) +{ + std::regex pattern(regex_str); + + return std::regex_match(itf_name, pattern); +} + +int get_grouped_interface_info(const std::string &itf_name, itf_group_info &info) +{ + std::regex pattern(regex_str); + std::smatch matches; + + if (!std::regex_match(itf_name, matches, pattern)) { + return -1; + } + + info.group_name = matches[1].str(); + info.index = std::stoi(matches[2].str()); + info.interface_name = matches[3].str(); + + return 0; +}; + +std::string format_interface_group(const std::string &group_name, unsigned int index, const std::string &interface_name) +{ + return ":" + group_name + "_" + std::to_string(index) + ":_" + interface_name; +}; +}; diff --git a/source/utils/json_attribute.cxx b/source/utils/json_attribute.cxx new file mode 100644 index 0000000..34d1239 --- /dev/null +++ b/source/utils/json_attribute.cxx @@ -0,0 +1,42 @@ +#include "json_attribute.hxx" + +json_attribute::json_attribute(std::string attribute) + : _attribute(std::move(attribute)) +{ +} + +int json_attribute::parse(const std::string &payload) +{ + try { + _json = nlohmann::json::parse(payload); + _json = _json[_attribute]; + } catch (nlohmann::json::parse_error &e) { + return -1; + } + return 0; +} + +int json_attribute::get_string(const std::string &key, std::string &str) +{ + return get(key, str); +} + +int json_attribute::get_int(const std::string &key, int &i) +{ + return get(key, i); +} + +int json_attribute::get_unsigned_int(const std::string &key, unsigned &u) +{ + return get(key, u); +} + +int json_attribute::get_double(const std::string &key, double &f) +{ + return get(key, f); +} + +int json_attribute::get_bool(const std::string &key, bool &b) +{ + return get(key, b); +} diff --git a/source/utils/json_attribute.hxx b/source/utils/json_attribute.hxx new file mode 100644 index 0000000..0179198 --- /dev/null +++ b/source/utils/json_attribute.hxx @@ -0,0 +1,31 @@ +#pragma once + +#include + +class json_attribute +{ +public: + json_attribute(std::string attribute); + + int parse(const std::string &payload); + + template int get(const std::string &key, T &t) + { + try { + t = _json[key].get(); + } catch (nlohmann::json::type_error &e) { + return -1; + } + return 0; + } + + int get_string(const std::string &key, std::string &str); + int get_int(const std::string &key, int &i); + int get_unsigned_int(const std::string &key, unsigned &u); + int get_double(const std::string &key, double &f); + int get_bool(const std::string &key, bool &b); + +private: + std::string _attribute; + nlohmann::json _json; +}; diff --git a/source/utils/topic.cxx b/source/utils/topic.cxx new file mode 100644 index 0000000..8e089e8 --- /dev/null +++ b/source/utils/topic.cxx @@ -0,0 +1,57 @@ +#include "topic.hxx" + +topic::topic(const std::string &topic) + : _topic(topic) +{ + std::stringstream strs(topic); + std::string buf; + std::array tmp_list; + + for (unsigned int i = 0; i < _max_depth; i++) { + if (!std::getline(strs, buf, '/')) { + spdlog::error("topic: invalid topic: {}", topic); + break; + } + tmp_list.at(i) = buf; + } + if (tmp_list[0] != "pza") { + spdlog::error("topic: invalid topic: {}", topic); + return; + } + _is_valid = true; + _list = tmp_list; +} + +std::string topic::regexify_topic(const std::string &topic) +{ + std::string t = topic; + + std::replace(t.begin(), t.end(), '+', '*'); + std::replace(t.begin(), t.end(), '#', '*'); + + return t; +} + +bool topic::topic_matches(const std::string &str, const std::string &fnmatchPattern) +{ + std::string regexPattern = _convertPattern(fnmatchPattern); + std::regex pattern(regexPattern); + + return std::regex_match(str, pattern); +} + +std::string topic::_convertPattern(const std::string &fnmatchPattern) +{ + std::string regexPattern; + + for (auto &ch : fnmatchPattern) { + if (ch == '*') + regexPattern += ".*"; + else if (ch == '/') + regexPattern += "\\/"; + else + regexPattern += ch; + } + regexPattern = "^" + regexPattern + "$"; + return regexPattern; +} diff --git a/source/utils/topic.hxx b/source/utils/topic.hxx new file mode 100644 index 0000000..0462eba --- /dev/null +++ b/source/utils/topic.hxx @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +#include + +class topic +{ +public: + explicit topic(const std::string &topic); + topic(const topic &) = delete; + topic(topic &&) = delete; + topic &operator=(const topic &) = delete; + topic &operator=(topic &&) = delete; + ~topic() = default; + + [[nodiscard]] bool is_valid() const + { + return _is_valid; + } + + [[nodiscard]] std::string get_topic() const + { + return _topic; + } + [[nodiscard]] std::string get_group() const + { + return _list[1]; + } + [[nodiscard]] std::string get_device_name() const + { + return _list[2]; + } + + static std::string regexify_topic(const std::string &topic); + static bool topic_matches(const std::string &str, const std::string &fnmatchPattern); + static std::string _convertPattern(const std::string &fnmatchPattern); + +private: + static constexpr unsigned int _max_depth = 3; + + std::string _topic; + bool _is_valid = false; + std::array _list; +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 69a965a..0f0bf99 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,25 +1,18 @@ -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) - -enable_testing() +include_guard() include(GoogleTest) -add_executable(unitest - main.cxx - connection.cxx - alias.cxx - interface.cxx - psu.cxx -) +find_package(GTest REQUIRED) -set_target_properties(unitest PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/test" +add_executable(unit_tests + unit/core.cxx + unit/client.cxx + unit/attribute.cxx ) -target_link_libraries(unitest ${LIBRARY_NAME} GTest::GTest) +target_link_libraries(unit_tests + GTest::gtest_main + ${LIBRARY_NAME} +) -gtest_discover_tests(unitest - DISCOVERY_TIMEOUT 60 - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test -) \ No newline at end of file +gtest_discover_tests(unit_tests) diff --git a/test/alias.cxx b/test/alias.cxx deleted file mode 100644 index 1e251b9..0000000 --- a/test/alias.cxx +++ /dev/null @@ -1,404 +0,0 @@ -#include -#include - -using namespace pza; - -class AliasTest : public ::testing::Test -{ -protected: - virtual void SetUp() - { - Core::RemoveAliases(); - } - - void loadAlias(const std::string &json) - { - Core::LoadAliases(json); - } - - void loadSystemFile(const std::string &file) - { - Core::LoadAliasesFromFile(file); - } - - void loadFile(const std::string &file) - { - const char *props = std::getenv("PROPS_PATH"); - - if (props) - Core::LoadAliasesFromFile(props + std::string("/alias/") + file); - else - Core::LoadAliasesFromFile("alias/" + file); - } - - void loadSystemFolder(const std::string &file) - { - Core::LoadAliasesFromDirectory(file); - } - - void loadFolder(const std::string &folder) - { - const char *props = std::getenv("PROPS_PATH"); - - if (props) - Core::LoadAliasesFromDirectory(props + std::string("/alias/") + folder); - else - Core::LoadAliasesFromDirectory("alias/" + folder); - } -}; - -class AliasFile : public AliasTest, - public ::testing::WithParamInterface> -{ - -}; - -class AliasFileFail : public AliasTest, - public ::testing::WithParamInterface -{ - -}; - -class AliasFolder : public AliasTest, - public ::testing::WithParamInterface> -{ - -}; - -using AliasTestFail = AliasTest; -using AliasSystemFileFail = AliasFileFail; -using AliasSystemFolderFail = AliasFolder; - -TEST_F(AliasTest, AliasSingle) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - } - })"); - EXPECT_EQ(Core::AliasesCount(), 1); - ASSERT_TRUE(Core::findAlias("local")); -} - -TEST_F(AliasTest, AliasMultiple) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - }, - "local2": { - "url": "localhost", - "port": 1883 - } - })"); - EXPECT_EQ(Core::AliasesCount(), 2); - ASSERT_TRUE(Core::findAlias("local")); - ASSERT_TRUE(Core::findAlias("local2")); -} - -TEST_F(AliasTest, AliasDuplicate) -{ - Alias *ptr; - - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - }, - "local": { - "url": "newlocalhost", - "port": 1885 - } - })"); - EXPECT_EQ(Core::AliasesCount(), 1); - ASSERT_TRUE(ptr = Core::findAlias("local")); - EXPECT_EQ(ptr->url, "newlocalhost"); - EXPECT_EQ(ptr->port, 1885); -} - -TEST_F(AliasTest, AliasDelete) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - }, - "local2": { - "url": "localhost", - "port": 1883 - } - })"); - EXPECT_EQ(Core::AliasesCount(), 2); - ASSERT_TRUE(Core::findAlias("local")); - Core::RemoveAlias("local"); - EXPECT_EQ(Core::AliasesCount(), 1); - EXPECT_FALSE(Core::findAlias("local")); -} - -TEST_F(AliasTest, AliasDeleteAll) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - }, - "local2": { - "url": "localhost", - "port": 1883 - } - })"); - EXPECT_EQ(Core::AliasesCount(), 2); - ASSERT_TRUE(Core::findAlias("local")); - ASSERT_TRUE(Core::findAlias("local2")); - Core::RemoveAliases(); - EXPECT_EQ(Core::AliasesCount(), 0); - EXPECT_FALSE(Core::findAlias("local")); - EXPECT_FALSE(Core::findAlias("local2")); -} - -TEST_F(AliasTest, AliasSingleInterface) -{ - std::string buf; - - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test" - } - } - })"); - ASSERT_TRUE(Core::findAlias("local")); - ASSERT_TRUE(Core::findAlias("local")->hasInterface("test")); - EXPECT_EQ(Core::findAlias("local")->getInterfaceTopic("test", buf), 0); - EXPECT_EQ(buf, "pza/machine/driver/test"); - EXPECT_EQ(Core::findAlias("local")->getInterfaceNameFromTopic("pza/machine/driver/test", buf), 0); - EXPECT_EQ(buf, "test"); -} - -TEST_F(AliasTest, AliasMultipleInterface) -{ - std::string buf1; - std::string buf2; - - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test", - "test2": "pza/machine/driver/test2" - } - } - })"); - ASSERT_TRUE(Core::findAlias("local")); - ASSERT_TRUE(Core::findAlias("local")->hasInterface("test")); - ASSERT_TRUE(Core::findAlias("local")->hasInterface("test2")); - EXPECT_EQ(Core::findAlias("local")->getInterfaceTopic("test", buf1), 0); - EXPECT_EQ(Core::findAlias("local")->getInterfaceTopic("test2", buf2), 0); - EXPECT_EQ(buf1, "pza/machine/driver/test"); - EXPECT_EQ(buf2, "pza/machine/driver/test2"); - EXPECT_EQ(Core::findAlias("local")->getInterfaceNameFromTopic("pza/machine/driver/test", buf1), 0); - EXPECT_EQ(Core::findAlias("local")->getInterfaceNameFromTopic("pza/machine/driver/test2", buf2), 0); - EXPECT_EQ(buf1, "test"); - EXPECT_EQ(buf2, "test2"); -} - -TEST_F(AliasTest, AliasDuplicateInterface) -{ - std::string buf; - - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test", - "test": "pza/machine/driver/test2" - } - } - })"); - ASSERT_TRUE(Core::findAlias("local")); - ASSERT_TRUE(Core::findAlias("local")->hasInterface("test")); - EXPECT_EQ(Core::findAlias("local")->getInterfaceTopic("test", buf), 0); - EXPECT_EQ(buf, "pza/machine/driver/test2"); - EXPECT_EQ(Core::findAlias("local")->getInterfaceNameFromTopic("pza/machine/driver/test2", buf), 0); - EXPECT_EQ(buf, "test"); -} - -TEST_F(AliasTest, AliasDuplicateAliasInterface) -{ - std::string buf; - - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test" - } - }, - "local": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test2" - } - } - })"); - ASSERT_TRUE(Core::findAlias("local")); - ASSERT_TRUE(Core::findAlias("local")->hasInterface("test")); - EXPECT_EQ(Core::findAlias("local")->getInterfaceTopic("test", buf), 0); - EXPECT_EQ(buf, "pza/machine/driver/test2"); - EXPECT_EQ(Core::findAlias("local")->getInterfaceNameFromTopic("pza/machine/driver/test2", buf), 0); - EXPECT_EQ(buf, "test"); -} - - -TEST_P(AliasFile, BadFormat) -{ - loadAlias(GetParam().first); - EXPECT_EQ(Core::AliasesCount(), GetParam().second); -} - -INSTANTIATE_TEST_SUITE_P(TestAliasFail, AliasFile, ::testing::Values( - std::make_pair(R"( - "local": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test", - "test2": "pza/machine/driver/test2" - } - } - })", 0), - std::make_pair(R"({ - "local" { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test", - "test2": "pza/machine/driver/test2" - } - } - })", 0), - std::make_pair(R"({ - "local": { - "url": "localhost" - "port": 1883, - "interfaces": { - "test": "pza/machine/driver/test", - "test2": "pza/machine/driver/test2" - } - } - })", 0), - std::make_pair(R"({ - "local": { - "url": "localhost", - "port": 1883, - "interfaces": - "test": "pza/machine/driver/test", - "test2": "pza/machine/driver/test2" - } - } - })", 0), - std::make_pair(R"({ - "local": { - "url": "localhost", - "port": 1883, - "test": "pza/machine/driver/test", - "test2": "pza/machine/driver/test2" - } - } - })", 0), - std::make_pair(R"({ - "local": { - "port": 1883 - } - })", 0), - std::make_pair(R"({ - })", 0), - std::make_pair(R"({ - "local": { - "url": "localhost" - } - })", 0), - std::make_pair(R"({ - "local": { - "url": "localhost" - } - })", 0), - std::make_pair(R"({ - "local": { - "url": "localhost", - "port": 1883 - }, - "local2" { - "url": "localhost", - "port": 1883 - } - })", 0), - std::make_pair(R"({ - "local": { - "url": "localhost", - "port": 1883 - }, - "local2": { - "port": 1883 - } - })", 1) -)); - -TEST_F(AliasTest, Good) -{ - loadFile("good.json"); - EXPECT_EQ(Core::AliasesCount(), 1); -} - -TEST_P(AliasFileFail, BadFile) -{ - loadFile(GetParam()); - EXPECT_EQ(Core::AliasesCount(), 0); -} - -TEST_P(AliasSystemFileFail, SystemFileFail) -{ - loadSystemFile(GetParam()); - EXPECT_EQ(Core::AliasesCount(), 0); -} - -INSTANTIATE_TEST_SUITE_P(TestAliasFileFail, AliasFileFail, ::testing::Values( - "empty.json", - "doesnotexist.json", - "folder_empty" -)); - -INSTANTIATE_TEST_SUITE_P(TestAliasSystemFileFail, AliasSystemFileFail, ::testing::Values( - "/dev/null", - "/dev/random", - "folder_empty" -)); - -TEST_P(AliasFolder, Folder) -{ - loadFolder(GetParam().first); - EXPECT_EQ(Core::AliasesCount(), GetParam().second); -} - -TEST_F(AliasTest, SystemFolderPermission) -{ - loadSystemFolder("/root"); - EXPECT_EQ(Core::AliasesCount(), 0); -} - -INSTANTIATE_TEST_SUITE_P(TestAliasFolder, AliasFolder, ::testing::Values( - std::make_pair("folder_single", 1), - std::make_pair("folder_multiple", 2), - std::make_pair("folder_multiple_duplicate", 1), - std::make_pair("folder_partial_good", 1), - std::make_pair("good.json", 0), - std::make_pair("folder_empty", 0) -)); \ No newline at end of file diff --git a/test/alias/folder_multiple/good.json b/test/alias/folder_multiple/good.json deleted file mode 100644 index 8d43b9c..0000000 --- a/test/alias/folder_multiple/good.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "good": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/py.psu.fake/My Psu" - } - } -} \ No newline at end of file diff --git a/test/alias/folder_multiple/good2.json b/test/alias/folder_multiple/good2.json deleted file mode 100644 index 7ba7f11..0000000 --- a/test/alias/folder_multiple/good2.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "good2": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/py.psu.fake/My Psu" - } - } -} diff --git a/test/alias/folder_multiple_duplicate/good.json b/test/alias/folder_multiple_duplicate/good.json deleted file mode 100644 index 8d43b9c..0000000 --- a/test/alias/folder_multiple_duplicate/good.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "good": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/py.psu.fake/My Psu" - } - } -} \ No newline at end of file diff --git a/test/alias/folder_multiple_duplicate/good2.json b/test/alias/folder_multiple_duplicate/good2.json deleted file mode 100644 index ebb7b15..0000000 --- a/test/alias/folder_multiple_duplicate/good2.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "good": { - "url": "localhost", - "port": 1884, - "interfaces": { - "test": "pza/machine/py.psu.fake/My Psu" - } - } -} diff --git a/test/alias/folder_partial_good/good.json b/test/alias/folder_partial_good/good.json deleted file mode 100644 index 8d43b9c..0000000 --- a/test/alias/folder_partial_good/good.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "good": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/py.psu.fake/My Psu" - } - } -} \ No newline at end of file diff --git a/test/alias/folder_single/good.json b/test/alias/folder_single/good.json deleted file mode 100644 index 8d43b9c..0000000 --- a/test/alias/folder_single/good.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "good": { - "url": "localhost", - "port": 1883, - "interfaces": { - "test": "pza/machine/py.psu.fake/My Psu" - } - } -} \ No newline at end of file diff --git a/test/alias/good.json b/test/alias/good.json deleted file mode 100644 index 809fbf9..0000000 --- a/test/alias/good.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "good": { - "url": "localhost", - "port": 1883, - "interfaces": { - "psu": "pza/machine/py.psu.fake/My Psu" - } - } -} diff --git a/test/connection.cxx b/test/connection.cxx deleted file mode 100644 index f800e16..0000000 --- a/test/connection.cxx +++ /dev/null @@ -1,176 +0,0 @@ -#include -#include - -using namespace pza; - -class BaseClient : public ::testing::Test, - public ::testing::WithParamInterface> -{ -protected: - virtual void SetUp() - { - auto url = GetParam().first; - auto port = GetParam().second; - client = new Client(url, port); - } - - Client *client; -}; - -class BaseClientAlias : public ::testing::Test -{ -protected: - - std::unique_ptr createClient(const std::string &alias) - { - return std::make_unique(alias); - } - - void loadAlias(const std::string &json) - { - Core::RemoveAliases(); - Core::LoadAliases(json); - } - - std::unique_ptr client; -}; - -using BaseConnSuccess = BaseClient; -using BaseConnFail = BaseClient; - -TEST_P(BaseConnSuccess, ConnectSuccess) -{ - EXPECT_EQ(client->connect(), 0); -} - -TEST_P(BaseConnFail, ConnectFail) -{ - EXPECT_EQ(client->connect(), -1); -} - -TEST_P(BaseConnSuccess, DisconnectSuccess) -{ - EXPECT_EQ(client->connect(), 0); - EXPECT_EQ(client->disconnect(), 0); -} - -TEST_P(BaseConnFail, DisconnectFail) -{ - EXPECT_EQ(client->connect(), -1); - EXPECT_EQ(client->disconnect(), -1); -} - -TEST_P(BaseConnSuccess, ReconnectSuccess) -{ - EXPECT_EQ(client->connect(), 0); - EXPECT_EQ(client->reconnect(), 0); - EXPECT_EQ(client->disconnect(), 0); - EXPECT_EQ(client->reconnect(), 0); -} - -TEST_P(BaseConnFail, ReconnectFail) -{ - EXPECT_EQ(client->connect(), -1); - EXPECT_EQ(client->reconnect(), -1); - EXPECT_EQ(client->disconnect(), -1); - EXPECT_EQ(client->reconnect(), -1); -} - -INSTANTIATE_TEST_SUITE_P(TestConnectionSuccess, BaseConnSuccess, - ::testing::Values( - std::make_pair("localhost", 1883), - std::make_pair("127.0.0.1", 1883))); - -INSTANTIATE_TEST_SUITE_P(TestConnectionFailure, BaseConnFail, - ::testing::Values( - std::make_pair("badlocalhost", 1883), - std::make_pair("", 1883), - std::make_pair("localhost", -1))); - -TEST_F(BaseClientAlias, ConnectSuccess) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - } - })"); - auto client = createClient("local"); - EXPECT_EQ(client->connect(), 0); -} - -TEST_F(BaseClientAlias, ConnectBadFormat) -{ - loadAlias(R"({ - "local": { - "ur": "localhost", - "port": 1883 - } - })"); - auto client = createClient("local"); - EXPECT_EQ(client->connect(), -1); -} - -TEST_F(BaseClientAlias, ConnectDoesNotExist) -{ - auto client = createClient("nothing"); - EXPECT_EQ(client->connect(), -1); -} - -TEST_F(BaseClientAlias, ConnectMultiple) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - }, - "local2": { - "url": "localhost", - "port": 1883 - } - })"); - - auto client = createClient("local"); - auto client2 = createClient("local2"); - - EXPECT_EQ(client->connect(), 0); - EXPECT_EQ(client2->connect(), 0); -} - -TEST_F(BaseClientAlias, ConnectToBadAndResetToAlias) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "prt": 1883 - } - })"); - - auto client = createClient("local"); - EXPECT_EQ(client->connect(), -1); - - loadAlias(R"({ - "local": { - "url": "localhost", - "port": 1883 - } - })"); - - client->resetAlias("local"); - EXPECT_EQ(client->connect(), 0); -} - -TEST_F(BaseClientAlias, ConnectToBadAndResetToRaw) -{ - loadAlias(R"({ - "local": { - "url": "localhost", - "prt": 1883 - } - })"); - - auto client = createClient("local"); - EXPECT_EQ(client->connect(), -1); - client->reset("localhost", "1883"); - EXPECT_EQ(client->connect(), 0); -} \ No newline at end of file diff --git a/test/interface.cxx b/test/interface.cxx deleted file mode 100644 index ea2af7c..0000000 --- a/test/interface.cxx +++ /dev/null @@ -1,52 +0,0 @@ -#include -#include -#include - -using namespace pza; - -// Using PSU as an example, but this applies to all interfaces - -class InterfaceTest : public ::testing::Test -{ -protected: - virtual void SetUp() - { - const char *props = std::getenv("PROPS_PATH"); - - if (!props) - props = ""; - Core::RemoveAliases(); - Core::LoadAliasesFromFile(props + std::string("alias/good.json")); - client = std::make_unique("good"); - ASSERT_EQ(client->connect(), 0); - psu = std::make_unique("psu"); - std::cout << client.get() << std::endl; - psu->bindToClient(client.get()); - ASSERT_TRUE(psu->isRunning()); - } - - std::unique_ptr client; - std::unique_ptr psu; -}; - -TEST_F(InterfaceTest, Disconnect) -{ - EXPECT_EQ(client->disconnect(), 0); - EXPECT_FALSE(psu->isRunning()); -} - -TEST_F(InterfaceTest, DisconnectAndConnect) -{ - EXPECT_EQ(client->disconnect(), 0); - EXPECT_FALSE(psu->isRunning()); - EXPECT_EQ(client->connect(), 0); - EXPECT_TRUE(psu->isRunning()); -} - -TEST_F(InterfaceTest, Reconnect) -{ - EXPECT_EQ(client->disconnect(), 0); - EXPECT_FALSE(psu->isRunning()); - EXPECT_EQ(client->reconnect(), 0); - EXPECT_TRUE(psu->isRunning()); -} \ No newline at end of file diff --git a/test/main.cxx b/test/main.cxx deleted file mode 100644 index 45acf01..0000000 --- a/test/main.cxx +++ /dev/null @@ -1,8 +0,0 @@ -#include - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/test/psu.cxx b/test/psu.cxx deleted file mode 100644 index d65e0fa..0000000 --- a/test/psu.cxx +++ /dev/null @@ -1,59 +0,0 @@ -#include -#include -#include - -using namespace pza; - -class PsuTest : public ::testing::Test -{ -protected: - virtual void SetUp() - { - const char *props = std::getenv("PROPS_PATH"); - - Core::RemoveAliases(); - - if (props) - Core::LoadAliasesFromFile(props + std::string("/alias/good.json")); - else - Core::LoadAliasesFromFile("alias/good.json"); - client = std::make_shared("good"); - ASSERT_EQ(client->connect(), 0); - psu = std::make_unique("psu"); - psu->bindToClient(client.get()); - ASSERT_TRUE(psu->isRunning()); - } - - std::shared_ptr client; - std::unique_ptr psu; -}; - -TEST_F(PsuTest, Enable) -{ - psu->enable.value.set(true); - EXPECT_EQ(psu->enable.value.get(), true); - psu->enable.value.set(false); - EXPECT_EQ(psu->enable.value.get(), false); -} - -TEST_F(PsuTest, VoltsValue) -{ - psu->volts.goal.set(4.2); - EXPECT_EQ(psu->volts.real.get(), 4.2); - EXPECT_EQ(psu->volts.goal.get(), 4.2); - - psu->volts.goal.set(8); - EXPECT_EQ(psu->volts.real.get(), 8); - EXPECT_EQ(psu->volts.goal.get(), 8); -} - -TEST_F(PsuTest, AmpsValue) -{ - psu->amps.goal.set(4.2); - EXPECT_EQ(psu->amps.real.get(), 4.2); - EXPECT_EQ(psu->amps.goal.get(), 4.2); - - psu->amps.goal.set(8); - EXPECT_EQ(psu->amps.real.get(), 8); - EXPECT_EQ(psu->amps.goal.get(), 8); -} \ No newline at end of file diff --git a/test/tree.json b/test/tree.json deleted file mode 100644 index f9aadb2..0000000 --- a/test/tree.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "machine": "machine", - "brokers": { - "my_broker": { - "addr": "localhost", - "port": 1883, - "interfaces": [ - { - "name" : "My Psu", - "driver" : "py.psu.fake" - }, - { - "name" : "My Psu 2", - "driver" : "py.psu.fake" - }, - { - "name" : "My Psu 3", - "driver" : "py.psu.fake" - }, - { - "name" : "My Psu 4", - "driver" : "py.psu.fake" - } - ] - } - } -} diff --git a/test/alias/empty.json b/test/unit/attribute.cxx similarity index 100% rename from test/alias/empty.json rename to test/unit/attribute.cxx diff --git a/test/alias/folder_partial_good/bad.json b/test/unit/client.cxx similarity index 100% rename from test/alias/folder_partial_good/bad.json rename to test/unit/client.cxx diff --git a/test/unit/core.cxx b/test/unit/core.cxx new file mode 100644 index 0000000..39c3cda --- /dev/null +++ b/test/unit/core.cxx @@ -0,0 +1,32 @@ +#include +#include + +TEST(core, log_level) +{ + pza::core::set_log_level(pza::core::log_level::trace); + EXPECT_EQ(pza::core::get_log_level(), pza::core::log_level::trace); + + pza::core::set_log_level(pza::core::log_level::debug); + EXPECT_EQ(pza::core::get_log_level(), pza::core::log_level::debug); + + pza::core::set_log_level(pza::core::log_level::info); + EXPECT_EQ(pza::core::get_log_level(), pza::core::log_level::info); + + pza::core::set_log_level(pza::core::log_level::warn); + EXPECT_EQ(pza::core::get_log_level(), pza::core::log_level::warn); + + pza::core::set_log_level(pza::core::log_level::err); + EXPECT_EQ(pza::core::get_log_level(), pza::core::log_level::err); + + pza::core::set_log_level(pza::core::log_level::critical); + EXPECT_EQ(pza::core::get_log_level(), pza::core::log_level::critical); + + pza::core::set_log_level(pza::core::log_level::off); + EXPECT_EQ(pza::core::get_log_level(), pza::core::log_level::off); +} + +TEST(core, version) +{ + EXPECT_NE(pza::core::get_version(), ""); + EXPECT_NE(pza::core::get_version_githash(), ""); +}