From b684d687c59b80585bc31b9c2f62603440181dc5 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Tue, 31 Oct 2023 15:34:57 +0100 Subject: [PATCH] Release v0.1.0 (#6) - Support for BPS --- .github/workflows/build.yml | 58 +++ .gitignore | 7 + CMakeLists.txt | 73 +++ LICENSE | 19 + README.md | 51 ++ cmake/cppcheck.cmake | 14 + cmake/lib.cmake | 7 + conan_profiles/x86_64_Cross_Windows | 22 + conan_profiles/x86_64_Linux | 6 + conanfile.py | 60 +++ docker/Dockerfile | 33 ++ examples/CMakeLists.txt | 18 + examples/README.md | 3 + examples/bps.cxx | 40 ++ scripts/build.sh | 96 ++++ scripts/docker_pull.sh | 3 + scripts/docker_registry.sh | 58 +++ scripts/install_dependencies.sh | 85 ++++ source/CMakeLists.txt | 33 ++ source/pza/core/attribute.cxx | 90 ++++ source/pza/core/attribute.hxx | 81 +++ source/pza/core/client.cxx | 463 ++++++++++++++++++ source/pza/core/client.hxx | 90 ++++ 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 | 29 ++ 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 + test/CMakeLists.txt | 25 + test/alias.cxx | 404 +++++++++++++++ test/alias/empty.json | 0 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/bad.json | 0 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 + 62 files changed, 3140 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/cppcheck.cmake create mode 100644 cmake/lib.cmake create mode 100644 conan_profiles/x86_64_Cross_Windows create mode 100644 conan_profiles/x86_64_Linux create mode 100644 conanfile.py create mode 100644 docker/Dockerfile create mode 100644 examples/CMakeLists.txt create mode 100644 examples/README.md create mode 100644 examples/bps.cxx create mode 100755 scripts/build.sh create mode 100755 scripts/docker_pull.sh create mode 100755 scripts/docker_registry.sh create mode 100755 scripts/install_dependencies.sh create mode 100644 source/CMakeLists.txt create mode 100644 source/pza/core/attribute.cxx create mode 100644 source/pza/core/attribute.hxx create mode 100644 source/pza/core/client.cxx create mode 100644 source/pza/core/client.hxx create mode 100644 source/pza/core/core.cxx create mode 100644 source/pza/core/core.hxx create mode 100644 source/pza/core/device.cxx create mode 100644 source/pza/core/device.hxx create mode 100644 source/pza/core/device_factory.cxx create mode 100644 source/pza/core/device_factory.hxx create mode 100644 source/pza/core/field.hxx create mode 100644 source/pza/core/grouped_interface.hxx create mode 100644 source/pza/core/interface.cxx create mode 100644 source/pza/core/interface.hxx create mode 100644 source/pza/devices/bps.cxx create mode 100644 source/pza/devices/bps.hxx create mode 100644 source/pza/interfaces/bps_chan_ctrl.cxx create mode 100644 source/pza/interfaces/bps_chan_ctrl.hxx create mode 100644 source/pza/interfaces/meter.cxx create mode 100644 source/pza/interfaces/meter.hxx create mode 100644 source/pza/utils/json.cxx create mode 100644 source/pza/utils/json.hxx create mode 100644 source/pza/utils/string.cxx create mode 100644 source/pza/utils/string.hxx create mode 100644 source/pza/utils/topic.cxx create mode 100644 source/pza/utils/topic.hxx create mode 100644 source/pza/version.hxx.in create mode 100644 test/CMakeLists.txt create mode 100644 test/alias.cxx create mode 100644 test/alias/empty.json create mode 100644 test/alias/folder_multiple/good.json create mode 100644 test/alias/folder_multiple/good2.json create mode 100644 test/alias/folder_multiple_duplicate/good.json create mode 100644 test/alias/folder_multiple_duplicate/good2.json create mode 100644 test/alias/folder_partial_good/bad.json create mode 100644 test/alias/folder_partial_good/good.json create mode 100644 test/alias/folder_single/good.json create mode 100644 test/alias/good.json create mode 100644 test/connection.cxx create mode 100644 test/interface.cxx create mode 100644 test/main.cxx create mode 100644 test/psu.cxx create mode 100644 test/tree.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c815e05 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,58 @@ +name: Build and test + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Conan installation + id: conan + uses: turtlebrowser/get-conan@v1.2 + + - name: Install platform deps + run: | + sudo apt-get update + sudo apt-get install -y mosquitto mosquitto-clients + sudo mosquitto -d -c /etc/mosquitto/mosquitto.conf + echo "loguru paho-mqtt pyserial pyudev pymodbus" | xargs -n1 > requirements.txt + pip install -r requirements.txt + + - name: Fetch platform + uses: actions/checkout@v3 + with: + repository: 'Panduza/panduza-py' + token: ${{ secrets.GITHUB_TOKEN }} + path: panduza-py + ref: 28-remonter-de-la-consommation-courante-hm310t + + - name: Run platform + run: | + sudo mkdir -p /etc/panduza/log + sudo chown -R $(whoami):$(whoami) /etc/panduza + cd panduza-py + pip install -e ./platform + nohup python3 platform/deploy/pza-py-platform-run.py ../test/tree.json & + + - name: Build Debug and Test + run: | + rm -rf build + ./scripts/build.sh + cd build + make test + + - name: Build Release and Test + run: | + rm -rf build + ./scripts/build.sh Release + cd build + make test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e8c37c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build* +examples/build* +examples/CMakeUserPresets.json +CMakeUserPresets.json +.vscode +Testing +conan_imports_manifest.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..458df0d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.25) + +project(PZACXX VERSION 0.1.0) +set(LIBRARY_NAME pza-cxx) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(cmake/cppcheck.cmake) + +add_compile_options(-Wall -Wextra) + +set(SPDLOG_FMT_EXTERNAL 1) +find_package(spdlog REQUIRED) +find_package(nlohmann_json REQUIRED) +find_package(PahoMqttCpp REQUIRED) +find_package(magic_enum 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) + 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 + magic_enum::magic_enum +) + +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 + $ + ) +endif() + +set_target_properties(${LIBRARY_NAME} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b5a2ef --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Panduza + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..308ebba --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Panduza C++ Library + +User library to develop C++ applications following Panduza API. + +## Dependencies + +### Build Deps + +| Package | Version | +| ------- | -------- | +| GCC | 13 | +| conan | 1.60 | +| cmake | >=3.25.0 | + +Library dependencies are managed wih Conan. + +To install conan, https://conan.io/downloads.html. + +### Library Deps + +| Package | Version | Runtime | +| ------------- | ------------ | -------- | +| paho-mqtt-cpp | 1.2.0 | ✔ | +| nlohmann JSON | 3.11.2 | ✔ | +| spdlog | 1.11.0 | ✔ | +| Google test | cci.20210126 | | +| cppcheck | 2.10 | | + +## Build + +``` +./scripts/install_dependencies.sh +./scripts/build.sh +``` + +``` +$ ./scripts/install_dependencies.sh --help +Usage: ./scripts/install_dependencies.sh [-t ] [-l ] [-b ] [-h] + -t --target Target platform (Windows or Linux). Default is Linux + -l --lib Library mode (Static or Shared). Default is Shared + -b --build Build mode (Debug or Release). Default is Debug + -h --help Display this help message +``` +``` +$ ./scripts/build.sh --help +Usage: ./scripts/build.sh [-t ] [-l ] [-b ] [-h] + -t --target Target platform (Windows or Linux). Default is Linux + -l --lib Library mode (Static or Shared). Default is Shared + -b --build Build mode (Debug or Release). Default is Debug + -h --help Display this help message +``` diff --git a/cmake/cppcheck.cmake b/cmake/cppcheck.cmake new file mode 100644 index 0000000..56ffbb1 --- /dev/null +++ b/cmake/cppcheck.cmake @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..22897e7 --- /dev/null +++ b/cmake/lib.cmake @@ -0,0 +1,7 @@ +set(CMAKE_DEBUG_POSTFIX -debug) +set_target_properties(${LIBRARY_NAME} + PROPERTIES + VERSION "${LIBRARY_VERSION}" + SOVERSION "${LIBRARY_VERSION_MAJOR}" + DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX} +) diff --git a/conan_profiles/x86_64_Cross_Windows b/conan_profiles/x86_64_Cross_Windows new file mode 100644 index 0000000..d7b63fe --- /dev/null +++ b/conan_profiles/x86_64_Cross_Windows @@ -0,0 +1,22 @@ +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/conan_profiles/x86_64_Linux b/conan_profiles/x86_64_Linux new file mode 100644 index 0000000..4721d8f --- /dev/null +++ b/conan_profiles/x86_64_Linux @@ -0,0 +1,6 @@ +[settings] +os=Linux +arch=x86_64 +compiler=gcc +compiler.version=13 +compiler.libcxx=libstdc++11 diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..060be8d --- /dev/null +++ b/conanfile.py @@ -0,0 +1,60 @@ +from conans import ConanFile +from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout +import os +import re + +class PzaCxx(ConanFile): + name = "libpza-cxx" + settings = "os", "compiler", "build_type", "arch" + options = { + "shared": [True, False], + "build_examples": [True, False] + } + default_options = { + "shared": True, + "build_examples": 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("nlohmann_json/[>=3.11.2]") + self.requires("magic_enum/[>=0.9.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()) + + def configure(self): + self.options["*"].shared = self.options.shared + + def generate(self): + tc = CMakeToolchain(self) + tc.variables["BUILD_EXAMPLES"] = self.options.build_examples + tc.filename = "pzacxx_toolchain.cmake" + tc.generate() + deps = CMakeDeps(self) + deps.generate() + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + 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 new file mode 100644 index 0000000..9a3294a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,33 @@ +FROM archlinux:base-devel-20231001.0.182270 + +RUN echo -e "\n[multilib]\nInclude = /etc/pacman.d/mirrorlist" | sudo tee -a /etc/pacman.conf +RUN pacman -Syu --noconfirm +RUN pacman -Sy --noconfirm \ + git \ + ninja \ + cmake \ + python \ + python-pip \ + wget \ + clang \ + libunwind \ + wine + +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 useradd -m mingw && echo "mingw:password" | chpasswd +RUN echo "mingw ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/myuser +USER mingw +WORKDIR /home/mingw +RUN git clone https://aur.archlinux.org/paru.git +RUN cd paru && makepkg -si --noconfirm +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 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..4703985 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,18 @@ +set(examples + bps +) + +foreach(example ${examples}) + add_executable(${example} ${example}.cxx) + set_target_properties(${example} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/bin" + ) + 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 new file mode 100644 index 0000000..64bbdc3 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +# Examples + +TBD \ No newline at end of file diff --git a/examples/bps.cxx b/examples/bps.cxx new file mode 100644 index 0000000..4a84205 --- /dev/null +++ b/examples/bps.cxx @@ -0,0 +1,40 @@ +#include +#include + +int main(int argc, char** argv) +{ + if (argc != 5) { + std::cerr << "Usage: " << argv[0] << "
" << std::endl; + return -1; + } + + const char *address = argv[1]; + int port = std::stoi(argv[2]); + const char *group = argv[3]; + const char *bps_name = argv[4]; + + pza::core::set_log_level(pza::core::log_level::debug); + + pza::client::ptr cli = std::make_shared(address, port); + + if (cli->connect() == -1) + return -1; + + pza::bps::ptr bps = std::make_shared(group, bps_name); + + if (cli->register_device(bps) == -1) + return -1; + + 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()); + } + + return cli->disconnect(); +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..f6fc9d4 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,96 @@ +#!/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 new file mode 100755 index 0000000..90ff9ec --- /dev/null +++ b/scripts/docker_pull.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker pull ghcr.io/panduza/pzacx-build-img:latest \ No newline at end of file diff --git a/scripts/docker_registry.sh b/scripts/docker_registry.sh new file mode 100755 index 0000000..1836595 --- /dev/null +++ b/scripts/docker_registry.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +IMAGE_NAME="pzacx-build-img" +PROJECT_ROOT_DIR="$(dirname "$(realpath "$0")")/.." + +TEMP=$(getopt -o u:p:h --long username:,password:,help -n "$0" -- "$@") +if [ $? != 0 ]; then + echo "Error processing arguments." >&2 + exit 1 +fi + +eval set -- "$TEMP" + +while true; do + case "$1" in + -u|--username) + GITHUB_USER="$2" + shift 2 + ;; + -p|--password) + PASSWORD="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [-u ] [-p ] [-h]" + echo " -u --username Github username" + echo " -p --password Github token" + echo " -h --help Display this help message" + exit 0 + ;; + --) + shift + break + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +if [ -z "$GITHUB_USER" ]; then + echo "Username not specified" + echo "-h or --help for more information" + exit 1 +fi + +if [ -z "$PASSWORD" ]; then + echo "Password not specified" + echo "-h or --help for more information" + exit 1 +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 diff --git a/scripts/install_dependencies.sh b/scripts/install_dependencies.sh new file mode 100755 index 0000000..ad273dc --- /dev/null +++ b/scripts/install_dependencies.sh @@ -0,0 +1,85 @@ +#!/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 new file mode 100644 index 0000000..efaa9ca --- /dev/null +++ b/source/CMakeLists.txt @@ -0,0 +1,33 @@ +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) + +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 +) diff --git a/source/pza/core/attribute.cxx b/source/pza/core/attribute.cxx new file mode 100644 index 0000000..466c78b --- /dev/null +++ b/source/pza/core/attribute.cxx @@ -0,0 +1,90 @@ +#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 new file mode 100644 index 0000000..dbec3e4 --- /dev/null +++ b/source/pza/core/attribute.hxx @@ -0,0 +1,81 @@ +#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 new file mode 100644 index 0000000..4ddb4de --- /dev/null +++ b/source/pza/core/client.cxx @@ -0,0 +1,463 @@ +#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 new file mode 100644 index 0000000..ed340fa --- /dev/null +++ b/source/pza/core/client.hxx @@ -0,0 +1,90 @@ +#pragma once + +#include +#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 new file mode 100644 index 0000000..7e50537 --- /dev/null +++ b/source/pza/core/core.cxx @@ -0,0 +1,18 @@ +#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 new file mode 100644 index 0000000..1324508 --- /dev/null +++ b/source/pza/core/core.hxx @@ -0,0 +1,29 @@ +#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 new file mode 100644 index 0000000..7e8422e --- /dev/null +++ b/source/pza/core/device.cxx @@ -0,0 +1,56 @@ +#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 new file mode 100644 index 0000000..5e5c4c4 --- /dev/null +++ b/source/pza/core/device.hxx @@ -0,0 +1,65 @@ +#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 new file mode 100644 index 0000000..81ce6ab --- /dev/null +++ b/source/pza/core/device_factory.cxx @@ -0,0 +1,21 @@ +#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 new file mode 100644 index 0000000..f877115 --- /dev/null +++ b/source/pza/core/device_factory.hxx @@ -0,0 +1,29 @@ +#pragma once + +#include + +#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; + }; +}; \ No newline at end of file diff --git a/source/pza/core/field.hxx b/source/pza/core/field.hxx new file mode 100644 index 0000000..c2538a9 --- /dev/null +++ b/source/pza/core/field.hxx @@ -0,0 +1,124 @@ +#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 new file mode 100644 index 0000000..3de5089 --- /dev/null +++ b/source/pza/core/grouped_interface.hxx @@ -0,0 +1,51 @@ +#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 new file mode 100644 index 0000000..9a89c0b --- /dev/null +++ b/source/pza/core/interface.cxx @@ -0,0 +1,46 @@ +#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 new file mode 100644 index 0000000..655bce6 --- /dev/null +++ b/source/pza/core/interface.hxx @@ -0,0 +1,33 @@ +#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 new file mode 100644 index 0000000..e7bcdfb --- /dev/null +++ b/source/pza/devices/bps.cxx @@ -0,0 +1,33 @@ +#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 new file mode 100644 index 0000000..b108084 --- /dev/null +++ b/source/pza/devices/bps.hxx @@ -0,0 +1,43 @@ +#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 new file mode 100644 index 0000000..e36ec51 --- /dev/null +++ b/source/pza/interfaces/bps_chan_ctrl.cxx @@ -0,0 +1,78 @@ +#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("goal"); + _att_voltage.add_ro_field("min"); + _att_voltage.add_ro_field("max"); + _att_voltage.add_ro_field("decimals"); + + _att_current.add_rw_field("goal"); + _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("goal").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("goal").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); +} \ No newline at end of file diff --git a/source/pza/interfaces/bps_chan_ctrl.hxx b/source/pza/interfaces/bps_chan_ctrl.hxx new file mode 100644 index 0000000..a1b0cde --- /dev/null +++ b/source/pza/interfaces/bps_chan_ctrl.hxx @@ -0,0 +1,35 @@ +#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 new file mode 100644 index 0000000..1921899 --- /dev/null +++ b/source/pza/interfaces/meter.cxx @@ -0,0 +1,34 @@ +#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 new file mode 100644 index 0000000..6f79e15 --- /dev/null +++ b/source/pza/interfaces/meter.hxx @@ -0,0 +1,20 @@ +#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 new file mode 100644 index 0000000..1ae8137 --- /dev/null +++ b/source/pza/utils/json.cxx @@ -0,0 +1,112 @@ +#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 new file mode 100644 index 0000000..72ecec6 --- /dev/null +++ b/source/pza/utils/json.hxx @@ -0,0 +1,18 @@ +#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 new file mode 100644 index 0000000..03d0b90 --- /dev/null +++ b/source/pza/utils/string.cxx @@ -0,0 +1,8 @@ +#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 new file mode 100644 index 0000000..3482756 --- /dev/null +++ b/source/pza/utils/string.hxx @@ -0,0 +1,11 @@ +#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 new file mode 100644 index 0000000..a772753 --- /dev/null +++ b/source/pza/utils/topic.cxx @@ -0,0 +1,24 @@ +#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 new file mode 100644 index 0000000..a9735ef --- /dev/null +++ b/source/pza/utils/topic.hxx @@ -0,0 +1,25 @@ +#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 new file mode 100644 index 0000000..c0a3695 --- /dev/null +++ b/source/pza/version.hxx.in @@ -0,0 +1,3 @@ +#pragma once + +#define PZACXX_VERSION "@PROJECT_VERSION@" \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..69a965a --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,25 @@ +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) + +enable_testing() + +include(GoogleTest) + +add_executable(unitest + main.cxx + connection.cxx + alias.cxx + interface.cxx + psu.cxx +) + +set_target_properties(unitest PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/test" +) + +target_link_libraries(unitest ${LIBRARY_NAME} GTest::GTest) + +gtest_discover_tests(unitest + DISCOVERY_TIMEOUT 60 + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test +) \ No newline at end of file diff --git a/test/alias.cxx b/test/alias.cxx new file mode 100644 index 0000000..1e251b9 --- /dev/null +++ b/test/alias.cxx @@ -0,0 +1,404 @@ +#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/empty.json b/test/alias/empty.json new file mode 100644 index 0000000..e69de29 diff --git a/test/alias/folder_multiple/good.json b/test/alias/folder_multiple/good.json new file mode 100644 index 0000000..8d43b9c --- /dev/null +++ b/test/alias/folder_multiple/good.json @@ -0,0 +1,9 @@ +{ + "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 new file mode 100644 index 0000000..7ba7f11 --- /dev/null +++ b/test/alias/folder_multiple/good2.json @@ -0,0 +1,9 @@ +{ + "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 new file mode 100644 index 0000000..8d43b9c --- /dev/null +++ b/test/alias/folder_multiple_duplicate/good.json @@ -0,0 +1,9 @@ +{ + "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 new file mode 100644 index 0000000..ebb7b15 --- /dev/null +++ b/test/alias/folder_multiple_duplicate/good2.json @@ -0,0 +1,9 @@ +{ + "good": { + "url": "localhost", + "port": 1884, + "interfaces": { + "test": "pza/machine/py.psu.fake/My Psu" + } + } +} diff --git a/test/alias/folder_partial_good/bad.json b/test/alias/folder_partial_good/bad.json new file mode 100644 index 0000000..e69de29 diff --git a/test/alias/folder_partial_good/good.json b/test/alias/folder_partial_good/good.json new file mode 100644 index 0000000..8d43b9c --- /dev/null +++ b/test/alias/folder_partial_good/good.json @@ -0,0 +1,9 @@ +{ + "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 new file mode 100644 index 0000000..8d43b9c --- /dev/null +++ b/test/alias/folder_single/good.json @@ -0,0 +1,9 @@ +{ + "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 new file mode 100644 index 0000000..809fbf9 --- /dev/null +++ b/test/alias/good.json @@ -0,0 +1,9 @@ +{ + "good": { + "url": "localhost", + "port": 1883, + "interfaces": { + "psu": "pza/machine/py.psu.fake/My Psu" + } + } +} diff --git a/test/connection.cxx b/test/connection.cxx new file mode 100644 index 0000000..f800e16 --- /dev/null +++ b/test/connection.cxx @@ -0,0 +1,176 @@ +#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 new file mode 100644 index 0000000..ea2af7c --- /dev/null +++ b/test/interface.cxx @@ -0,0 +1,52 @@ +#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 new file mode 100644 index 0000000..45acf01 --- /dev/null +++ b/test/main.cxx @@ -0,0 +1,8 @@ +#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 new file mode 100644 index 0000000..d65e0fa --- /dev/null +++ b/test/psu.cxx @@ -0,0 +1,59 @@ +#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 new file mode 100644 index 0000000..f9aadb2 --- /dev/null +++ b/test/tree.json @@ -0,0 +1,27 @@ +{ + "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" + } + ] + } + } +}