From 276701bdbbb9e9540373a8c155a2fdca6e2e6e89 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Mon, 5 Dec 2022 19:07:39 +0100 Subject: [PATCH 01/11] Initial version 0.1.0 of the lib --- .github/workflows/build.yml | 58 +++ .gitignore | 6 + CMakeLists.txt | 57 +++ Dockerfile | 12 + LICENSE | 19 + README.md | 69 +++ cmake/cppcheck.cmake | 14 + cmake/lib.cmake | 7 + conan_profiles/x86_64_Cross_Windows | 23 + conan_profiles/x86_64_Linux | 13 + conanfile.py | 56 +++ docker.sh | 29 ++ examples/CMakeLists.txt | 11 + examples/README.md | 3 + examples/bps.cxx | 45 ++ run.sh | 12 + scripts/build.sh | 108 +++++ source/CMakeLists.txt | 33 ++ source/pza/core/attribute.cxx | 90 ++++ source/pza/core/attribute.hxx | 81 ++++ source/pza/core/client.cxx | 410 ++++++++++++++++++ source/pza/core/client.hxx | 88 ++++ source/pza/core/core.cxx | 18 + source/pza/core/core.hxx | 29 ++ source/pza/core/device.cxx | 55 +++ source/pza/core/device.hxx | 65 +++ source/pza/core/device_factory.cxx | 18 + source/pza/core/device_factory.hxx | 30 ++ 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 ++ 61 files changed, 2971 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Dockerfile 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 100755 docker.sh create mode 100644 examples/CMakeLists.txt create mode 100644 examples/README.md create mode 100644 examples/bps.cxx create mode 100755 run.sh create mode 100755 scripts/build.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..47ca150 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build* +examples/build* +examples/CMakeUserPresets.json +CMakeUserPresets.json +.vscode +Testing diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..876d098 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,57 @@ +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(GTest REQUIRED) +find_package(cppcheck REQUIRED) +find_package(magic_enum REQUIRED) + +if (CMAKE_SYSTEM_NAME MATCHES "Windows" OR 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") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static") + set(CMAKE_CROSSCOMPILING_EMULATOR "wine") +endif() + +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-static> + $<$,$>>:PahoMqttCpp::paho-mqttpp3> + spdlog::spdlog + nlohmann_json::nlohmann_json + magic_enum::magic_enum +) + +set_target_properties(${LIBRARY_NAME} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..758c146 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM pza-cpp-img:latest + +ARG USER_ID +ARG GROUP_ID + +RUN groupadd -g $GROUP_ID dummy +RUN useradd -m -u $USER_ID -g $GROUP_ID -s /bin/bash dummy +RUN echo 'dummy:dummy' | chpasswd + +USER dummy + +WORKDIR /work \ No newline at end of file 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..516dc40 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Panduza C++ Library + +User library to develop C++ applications following Panduza API. + +## Dependencies + +### Build Deps + +| Package | Version | +| ------- | ------- | +| conan | 1.58 | +| 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 | | + +## Version + +The version of the library is fetched from the top line of CHANGELOG.md. + +## Build + +To build the library in Debug or Release mode: + +``` +mkdir build +cd build +conan install .. --build=missing -s build_type= +cmake -DCMAKE_INSTALL_PREFIX=./install -DCMAKE_BUILD_TYPE= -DCMAKE_TOOLCHAIN_FILE=.//generators/conan_toolchain.cmake .. +cmake --build . +cmake --install . +``` + +To build and export the library as a conan package: + +``` +mkdir build +cd build +conan create .. --build=missing -s build_type= +``` + +## Test + +After building the library, you can test it with: + +``` +cd build +make test +``` + +## Check + +You can use cppcheck to examine the code. + +``` +cd build +make check +``` \ No newline at end of file diff --git a/cmake/cppcheck.cmake b/cmake/cppcheck.cmake new file mode 100644 index 0000000..3a7f938 --- /dev/null +++ b/cmake/cppcheck.cmake @@ -0,0 +1,14 @@ +# Add a target for cppcheck +find_program(CPPCHECK_EXECUTABLE cppcheck) +if(CPPCHECK_EXECUTABLE) + # cppcheck igner never used functions + 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() 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..0f953d4 --- /dev/null +++ b/conan_profiles/x86_64_Cross_Windows @@ -0,0 +1,23 @@ +$toolchain=/usr/x86_64-w64-mingw32 # Adjust this path +target_host=x86_64-w64-mingw32 +cc_compiler=gcc +cxx_compiler=g++ + +[env] +CONAN_CMAKE_FIND_ROOT_PATH=$toolchain +CHOST=$target_host +AR=$target_host-ar +AS=$target_host-as +RANLIB=$target_host-ranlib +CC=$target_host-$cc_compiler +CXX=$target_host-$cxx_compiler +STRIP=$target_host-strip +RC=$target_host-windres + +# 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..d17b05f --- /dev/null +++ b/conan_profiles/x86_64_Linux @@ -0,0 +1,13 @@ +[settings] +os=Linux +os_build=Linux +arch=x86_64 +arch_build=x86_64 +compiler=gcc +compiler.version=13 +compiler.libcxx=libstdc++11 +build_type=Release +[options] +[build_requires] +[env] + diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..4932759 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,56 @@ +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": False + } + 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("gtest/cci.20210126") + self.requires("cppcheck/[>=2.10]") + self.requires("magic_enum/[>=0.9.2]") + + def layout(self): + cmake_layout(self, build_folder=os.getcwd()) + + def configure(self): + if self.settings.os == "Windows": + self.options["*"].shared = False + else: + self.options["*"].shared = self.options.shared + + def generate(self): + tc = CMakeToolchain(self) + tc.variables["BUILD_EXAMPLES"] = self.options.build_examples + 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 package_info(self): + self.cpp_info.name = "Panduza C++ Library" + suffix = "-debug" if self.settings.build_type == "Debug" else "" + self.cpp_info.libs = [f"pza-cxx{suffix}"] diff --git a/docker.sh b/docker.sh new file mode 100755 index 0000000..9539ac4 --- /dev/null +++ b/docker.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +CPP_IMG_DEP="pza-cpp-img" +IMAGE_NAME="pza-libcxx-img" +CONTAINER_NAME="pza-libcxx" + +IMAGE=$(docker images -q $CPP_IMG_DEP) +if [ -z "$IMAGE" ]; then + echo "Docker image $CPP_IMG_DEP not found. You need to build the $CPP_IMG_DEP image first." + exit 1 +fi + +# Check if the Docker image exists +IMAGE=$(docker images -q $IMAGE_NAME) +if [ -z "$IMAGE" ]; then + echo "Docker image not found. Building image from Dockerfile..." + docker build --no-cache --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) -t $IMAGE_NAME . +fi + +# Check if the Docker container exists +CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME) +if [ -z "$CONTAINER" ]; then + # If the container does not exist, create and start it + echo "Docker container not found. Creating and starting container..." + docker run --rm -v $(pwd):/work -di --name $CONTAINER_NAME $IMAGE_NAME + fi + +echo "Running docker exec on the container..." +docker exec -it $CONTAINER_NAME bash -c "/work/scripts/build.sh $*" diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..fca323a --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,11 @@ +set(examples + bps +) + +foreach(example ${examples}) + add_executable(${example} ${example}.cxx) + set_target_properties(${example} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples" + ) + target_link_libraries(${example} ${LIBRARY_NAME}) +endforeach() \ No newline at end of file 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..ff1b6b2 --- /dev/null +++ b/examples/bps.cxx @@ -0,0 +1,45 @@ +#include +#include +#include + +int main(void) +{ + pza::core::set_log_level(pza::core::log_level::trace); + + pza::client::ptr cli = std::make_shared("localhost", 1883); + + if (cli->connect() == -1) { + return -1; + } + + pza::bps::ptr bps = std::make_shared("default", "MY BPS 1"); + + if (cli->register_device(bps) == -1) + return -1; + + for (size_t i = 0; i < 1; 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()); +// bps_channel->ctrl.set_enable(true); +// bps_channel->ctrl.set_voltage(3.3); +// bps_channel->ctrl.set_current(5.0); +// spdlog::info(" Voltage: {}", bps_channel->voltmeter.get_measure()); +// spdlog::info(" Current: {}", bps_channel->ampermeter.get_measure()); +// spdlog::info(" Enabled: {}", bps_channel->ctrl.get_enable()); + + //bps_channel->voltmeter.set_measure_polling_cycle(2); + //bps_channel->ampermeter.set_measure_polling_cycle(2); + bps_channel->ctrl.set_enable_polling_cycle(2); + } + + spdlog::info("\n\nOK\n\n"); + + return 0; +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..0c385a9 --- /dev/null +++ b/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Check if the Docker container exists +CONTAINER=$(docker ps -aq -f name=pza-libcxx) +if [ -z "$CONTAINER" ]; then + # If the container does not exist, create and start it + echo "Docker container not found. Running container..." + docker run --rm --network=host -v $(pwd):/work -di --name pza-libcxx pza-cpp-img + fi + +echo "Running docker exec on the container..." +docker exec -it pza-libcxx bash -c "/work/$*" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..64d2cf1 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +BUILD_DIR_LINUX="build" +BUILD_DIR_WINDOWS="buildwin" +BUILD_TYPE="Debug" +BUILD_EXAMPLES="False" +SHARED="True" +TARGET="Linux" +BUILD_DIR=$BUILD_DIR_LINUX + +function set_build_dir { + if [ "$TARGET" = "Windows" ]; then + BUILD_DIR=$BUILD_DIR_WINDOWS + fi + if [ "$SHARED" = "False" ]; then + BUILD_DIR="$BUILD_DIR-static" + fi +} + +function install_deps { + echo "Installing dependencies for $TARGET..." + if [ "$TARGET" = "Windows" ]; then + EXTRA_CONAN_ARGS="-pr:h ../conan_profiles/x86_64_Cross_Windows" + fi + mkdir -p $BUILD_DIR + cd $BUILD_DIR + conan install .. -o shared=$SHARED -o build_examples=$BUILD_EXAMPLES -s build_type=$BUILD_TYPE --build=missing -pr:b ../conan_profiles/x86_64_Linux $EXTRA_CONAN_ARGS +} + +function build { + echo "Building for $TARGET..." + cd $BUILD_DIR + conan build .. +} + +function clean { + echo "Cleaning build directory for $TARGET" + set_build_dir + rm -rf $BUILD_DIR +} + +function usage { + echo "Usage: $0 [-t ] [-r] [-s] [-d] [-e] [-c] [-h]" + echo " -t Target platform (Windows or Linux). Default is Linux" + echo " -r Build in Release mode. Default is Debug" + echo " -s Build in Static mode. Default is Shared" + echo " -d Install dependencies" + echo " -e Build examples" + echo " -c Clean build directory" + echo " -h Display this help message" +} + +# Parse command line arguments +while getopts "t:rscdebh" opt; do + case $opt in + t) + TARGET=$OPTARG + if [ "$TARGET" != "Windows" ] && [ "$TARGET" != "Linux" ]; then + echo "Invalid target: $TARGET" >&2 + usage + exit 1 + fi + ;; + r) + BUILD_TYPE="Release" + ;; + s) + SHARED="False" + ;; + c) + CLEAN="True" + ;; + d) + DEPS="True" + ;; + e) + BUILD_EXAMPLES="True" + ;; + h) + usage + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + exit 1 + ;; + esac +done + +set_build_dir + +if [ "$CLEAN" = "True" ]; then + clean + exit 0 +fi + +if [ "$DEPS" = "True" ]; then + install_deps + exit 0 +fi + +build \ No newline at end of file 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..142852c --- /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 interface; + + 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..4095c79 --- /dev/null +++ b/source/pza/core/client.cxx @@ -0,0 +1,410 @@ +#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_devices(); + 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::_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_devices(void) +{ + bool ret; + std::condition_variable cv; + std::unique_lock lock(_mtx); + + if (is_connected() == false) { + spdlog::error("scan failed. Not connected to {}", _addr); + return -1; + } + + _scan_device_count_expected = 0; + _scan_device_results.clear(); + + spdlog::debug("scanning for devices on {}...", _addr); + + _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(); + }); + + _subscribe("pza/server/+/+/atts/info", [&](const mqtt::const_message_ptr &msg) { + std::string payload = msg->get_payload_str(); + std::string topic = msg->get_topic(); + unsigned int val; + + spdlog::debug("received platform info: {}", payload); + 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(); + }); + + _publish("pza", "*"); + + 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"); + _unsubscribe("pza/server/+/+/atts/info"); + + if (ret == false) { + spdlog::error("timed out waiting for scan results"); + spdlog::debug("Expected {} devices, got {}", _scan_device_count_expected, _scan_device_results.size()); + return -1; + } + + 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(); + }); + + _publish(device->_get_device_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::trace("_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; + } + + 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..2b3842f --- /dev/null +++ b/source/pza/core/client.hxx @@ -0,0 +1,88 @@ +#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 interface; + + explicit client(const std::string &addr, int port, const std::string &id = ""); + + int connect(void); + int disconnect(void); + int scan_devices(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_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..0ec8b7c --- /dev/null +++ b/source/pza/core/device.cxx @@ -0,0 +1,55 @@ +#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(interface &interface) +{ + _interfaces[interface._name] = &interface; +} \ 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..54b969d --- /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 interface; + + 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(interface &interface); + + 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..82b6776 --- /dev/null +++ b/source/pza/core/device_factory.cxx @@ -0,0 +1,18 @@ +#include "device_factory.hxx" + +using namespace pza; + +std::map device_factory::_factory_map = { + { "bps", device_factory::allocate_device } +}; + +device::ptr device_factory::create_device(const std::string &family, const std::string &group, const std::string &name) +{ + auto it = _factory_map.find(family); + 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..2a036c9 --- /dev/null +++ b/source/pza/core/device_factory.hxx @@ -0,0 +1,30 @@ +#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; + static std::map _factory_map; + }; +}; \ 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..df36b8f --- /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..394b484 --- /dev/null +++ b/source/pza/core/interface.cxx @@ -0,0 +1,46 @@ +#include "interface.hxx" +#include +#include + +using namespace pza; + +interface::interface(device *device, const std::string &name) + : _device(device), + _name(name) +{ + _topic_base = _device->_get_base_topic() + "/" + _name; + _topic_cmd = _topic_base + "/cmds/set"; +} + +void interface::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"); + } + _device->get_client()->_unsubscribe(topic); + if (received) { + _device->get_client()->_subscribe(topic, std::bind(&attribute::on_message, &attribute, std::placeholders::_1)); + } +} + +void interface::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..b0f63ea --- /dev/null +++ b/source/pza/core/interface.hxx @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include + +namespace pza +{ + class device; + + class interface + { + public: + friend class device; + + interface(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..d28737a --- /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) + : interface(device, name), + _volts("volts"), + _amps("amps"), + _enable("enable") +{ + _volts.add_rw_field("goal"); + _volts.add_ro_field("min"); + _volts.add_ro_field("max"); + _volts.add_ro_field("decimals"); + + _amps.add_rw_field("goal"); + _amps.add_ro_field("min"); + _amps.add_ro_field("max"); + _amps.add_ro_field("decimals"); + + _enable.add_rw_field("value"); + _enable.add_rw_field("polling_cycle"); + + register_attributes({&_volts, &_amps, &_enable}); +} + +int bps_chan_ctrl::set_voltage(double volts) +{ + double min = _volts.get_field("min").get(); + double max = _volts.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 _volts.get_field("goal").set(volts); +} + +int bps_chan_ctrl::set_current(double amps) +{ + double min = _amps.get_field("min").get(); + double max = _amps.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 _amps.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..3ae6f7c --- /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 interface + { + 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 _volts; + attribute _amps; + 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..cb25e3c --- /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) + : interface(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..07894b4 --- /dev/null +++ b/source/pza/interfaces/meter.hxx @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace pza { + class meter : public interface + { + 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" + } + ] + } + } +} From 6f41c5e5722849542e824e7b9d46ab867fd1b440 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Sun, 1 Oct 2023 14:25:58 +0200 Subject: [PATCH 02/11] Conan cleanup --- conanfile.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/conanfile.py b/conanfile.py index 4932759..e82a7d3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -37,6 +37,7 @@ def configure(self): 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() @@ -48,9 +49,4 @@ def build(self): def package(self): cmake = CMake(self) - cmake.install() - - def package_info(self): - self.cpp_info.name = "Panduza C++ Library" - suffix = "-debug" if self.settings.build_type == "Debug" else "" - self.cpp_info.libs = [f"pza-cxx{suffix}"] + cmake.install() \ No newline at end of file From 4daf993a068098013497211aac790859897d80fd Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Sun, 1 Oct 2023 21:14:59 +0200 Subject: [PATCH 03/11] Windows build fixes - Fix conflicting interface keywork in MinGW - Improve build system --- .gitignore | 1 + CMakeLists.txt | 31 ++++++++++++++++++------- cmake/cppcheck.cmake | 26 ++++++++++----------- conan_profiles/x86_64_Cross_Windows | 11 ++++----- conan_profiles/x86_64_Linux | 7 ------ conanfile.py | 22 ++++++++++++------ examples/CMakeLists.txt | 11 +++++++-- examples/bps.cxx | 4 +++- scripts/build.sh | 2 ++ source/pza/core/attribute.hxx | 2 +- source/pza/core/client.hxx | 2 +- source/pza/core/device.cxx | 4 ++-- source/pza/core/device.hxx | 6 ++--- source/pza/core/interface.cxx | 6 ++--- source/pza/core/interface.hxx | 4 ++-- source/pza/interfaces/bps_chan_ctrl.cxx | 2 +- source/pza/interfaces/bps_chan_ctrl.hxx | 2 +- source/pza/interfaces/meter.cxx | 2 +- source/pza/interfaces/meter.hxx | 2 +- 19 files changed, 87 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 47ca150..6e8c37c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ examples/CMakeUserPresets.json CMakeUserPresets.json .vscode Testing +conan_imports_manifest.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 876d098..96bcc75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,18 +14,25 @@ set(SPDLOG_FMT_EXTERNAL 1) find_package(spdlog REQUIRED) find_package(nlohmann_json REQUIRED) find_package(PahoMqttCpp REQUIRED) -find_package(GTest REQUIRED) -find_package(cppcheck REQUIRED) find_package(magic_enum REQUIRED) -if (CMAKE_SYSTEM_NAME MATCHES "Windows" OR CMAKE_SYSTEM_NAME MATCHES "Linux" AND NOT BUILD_SHARED_LIBS) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-allow-multiple-definition") +if (CMAKE_SYSTEM_NAME MATCHES "Linux") + find_package(GTest REQUIRED) + find_package(cppcheck REQUIRED) endif() + if (CMAKE_SYSTEM_NAME MATCHES "Windows") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static") - set(CMAKE_CROSSCOMPILING_EMULATOR "wine") + 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}) @@ -44,13 +51,21 @@ target_include_directories(${LIBRARY_NAME} PUBLIC ) target_link_libraries(${LIBRARY_NAME} - $<$>,$>:PahoMqttCpp::paho-mqttpp3-static> - $<$,$>>:PahoMqttCpp::paho-mqttpp3> + $<$: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) + add_custom_command(TARGET ${LIBRARY_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_BINARY_DIR}/bin/*.dll + $ + ) +endif() + set_target_properties(${LIBRARY_NAME} PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} diff --git a/cmake/cppcheck.cmake b/cmake/cppcheck.cmake index 3a7f938..56ffbb1 100644 --- a/cmake/cppcheck.cmake +++ b/cmake/cppcheck.cmake @@ -1,14 +1,14 @@ -# Add a target for cppcheck -find_program(CPPCHECK_EXECUTABLE cppcheck) -if(CPPCHECK_EXECUTABLE) - # cppcheck igner never used functions - 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") +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/conan_profiles/x86_64_Cross_Windows b/conan_profiles/x86_64_Cross_Windows index 0f953d4..d7b63fe 100644 --- a/conan_profiles/x86_64_Cross_Windows +++ b/conan_profiles/x86_64_Cross_Windows @@ -1,19 +1,18 @@ -$toolchain=/usr/x86_64-w64-mingw32 # Adjust this path target_host=x86_64-w64-mingw32 -cc_compiler=gcc -cxx_compiler=g++ [env] -CONAN_CMAKE_FIND_ROOT_PATH=$toolchain CHOST=$target_host AR=$target_host-ar AS=$target_host-as RANLIB=$target_host-ranlib -CC=$target_host-$cc_compiler -CXX=$target_host-$cxx_compiler +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 diff --git a/conan_profiles/x86_64_Linux b/conan_profiles/x86_64_Linux index d17b05f..4721d8f 100644 --- a/conan_profiles/x86_64_Linux +++ b/conan_profiles/x86_64_Linux @@ -1,13 +1,6 @@ [settings] os=Linux -os_build=Linux arch=x86_64 -arch_build=x86_64 compiler=gcc compiler.version=13 compiler.libcxx=libstdc++11 -build_type=Release -[options] -[build_requires] -[env] - diff --git a/conanfile.py b/conanfile.py index e82a7d3..4e0507d 100644 --- a/conanfile.py +++ b/conanfile.py @@ -21,18 +21,16 @@ 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("gtest/cci.20210126") - self.requires("cppcheck/[>=2.10]") 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): - if self.settings.os == "Windows": - self.options["*"].shared = False - else: - self.options["*"].shared = self.options.shared + self.options["*"].shared = self.options.shared def generate(self): tc = CMakeToolchain(self) @@ -49,4 +47,14 @@ def build(self): def package(self): cmake = CMake(self) - cmake.install() \ No newline at end of file + 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/examples/CMakeLists.txt b/examples/CMakeLists.txt index fca323a..f4150bf 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -5,7 +5,14 @@ set(examples foreach(example ${examples}) add_executable(${example} ${example}.cxx) set_target_properties(${example} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/examples" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples/bin" ) target_link_libraries(${example} ${LIBRARY_NAME}) -endforeach() \ No newline at end of file + 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}/bin/*.dll + $ + ) + endif() +endforeach() diff --git a/examples/bps.cxx b/examples/bps.cxx index ff1b6b2..d4145c2 100644 --- a/examples/bps.cxx +++ b/examples/bps.cxx @@ -36,10 +36,12 @@ int main(void) //bps_channel->voltmeter.set_measure_polling_cycle(2); //bps_channel->ampermeter.set_measure_polling_cycle(2); - bps_channel->ctrl.set_enable_polling_cycle(2); + //bps_channel->ctrl.set_enable_polling_cycle(2); } spdlog::info("\n\nOK\n\n"); + cli->disconnect(); + return 0; } diff --git a/scripts/build.sh b/scripts/build.sh index 64d2cf1..f0f04d4 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -21,6 +21,8 @@ function install_deps { echo "Installing dependencies for $TARGET..." if [ "$TARGET" = "Windows" ]; then EXTRA_CONAN_ARGS="-pr:h ../conan_profiles/x86_64_Cross_Windows" + else + EXTRA_CONAN_ARGS="-pr:h ../conan_profiles/x86_64_Linux" fi mkdir -p $BUILD_DIR cd $BUILD_DIR diff --git a/source/pza/core/attribute.hxx b/source/pza/core/attribute.hxx index 142852c..dbec3e4 100644 --- a/source/pza/core/attribute.hxx +++ b/source/pza/core/attribute.hxx @@ -14,7 +14,7 @@ namespace pza class attribute { public: - friend class interface; + friend class itface; explicit attribute(const std::string &name); diff --git a/source/pza/core/client.hxx b/source/pza/core/client.hxx index 2b3842f..6c51f47 100644 --- a/source/pza/core/client.hxx +++ b/source/pza/core/client.hxx @@ -25,7 +25,7 @@ namespace pza using ptr = std::shared_ptr; friend class device; - friend class interface; + friend class itface; explicit client(const std::string &addr, int port, const std::string &id = ""); diff --git a/source/pza/core/device.cxx b/source/pza/core/device.cxx index 0ec8b7c..0de2bf2 100644 --- a/source/pza/core/device.cxx +++ b/source/pza/core/device.cxx @@ -49,7 +49,7 @@ int device::_set_identity(const std::string &payload) return 0; } -void device::register_interface(interface &interface) +void device::register_interface(itface &itface) { - _interfaces[interface._name] = &interface; + _interfaces[itface._name] = &itface; } \ No newline at end of file diff --git a/source/pza/core/device.hxx b/source/pza/core/device.hxx index 54b969d..5e5c4c4 100644 --- a/source/pza/core/device.hxx +++ b/source/pza/core/device.hxx @@ -19,7 +19,7 @@ namespace pza using ptr = std::shared_ptr; friend class client; - friend class interface; + friend class itface; enum class state : unsigned int { @@ -37,7 +37,7 @@ namespace pza void reset(); enum state get_state() { return _state; } - void register_interface(interface &interface); + void register_interface(itface &itface); protected: device(const std::string &group, const std::string &name); @@ -58,7 +58,7 @@ namespace pza std::string _base_topic; std::string _device_topic; - std::map _interfaces; + std::map _interfaces; enum state _state = state::orphan; }; diff --git a/source/pza/core/interface.cxx b/source/pza/core/interface.cxx index 394b484..d09850d 100644 --- a/source/pza/core/interface.cxx +++ b/source/pza/core/interface.cxx @@ -4,7 +4,7 @@ using namespace pza; -interface::interface(device *device, const std::string &name) +itface::itface(device *device, const std::string &name) : _device(device), _name(name) { @@ -12,7 +12,7 @@ interface::interface(device *device, const std::string &name) _topic_cmd = _topic_base + "/cmds/set"; } -void interface::register_attribute(attribute &attribute) +void itface::register_attribute(attribute &attribute) { std::condition_variable cv; std::unique_lock lock(_mtx); @@ -38,7 +38,7 @@ void interface::register_attribute(attribute &attribute) } } -void interface::register_attributes(const std::vector &list) +void itface::register_attributes(const std::vector &list) { for (auto const &it : list) { register_attribute(*it); diff --git a/source/pza/core/interface.hxx b/source/pza/core/interface.hxx index b0f63ea..655bce6 100644 --- a/source/pza/core/interface.hxx +++ b/source/pza/core/interface.hxx @@ -9,12 +9,12 @@ namespace pza { class device; - class interface + class itface { public: friend class device; - interface(device *device, const std::string &name); + itface(device *device, const std::string &name); void register_attribute(attribute &attribute); void register_attributes(const std::vector &list); diff --git a/source/pza/interfaces/bps_chan_ctrl.cxx b/source/pza/interfaces/bps_chan_ctrl.cxx index d28737a..7732439 100644 --- a/source/pza/interfaces/bps_chan_ctrl.cxx +++ b/source/pza/interfaces/bps_chan_ctrl.cxx @@ -5,7 +5,7 @@ using namespace pza; bps_chan_ctrl::bps_chan_ctrl(device *device, const std::string &name) - : interface(device, name), + : itface(device, name), _volts("volts"), _amps("amps"), _enable("enable") diff --git a/source/pza/interfaces/bps_chan_ctrl.hxx b/source/pza/interfaces/bps_chan_ctrl.hxx index 3ae6f7c..d5346f3 100644 --- a/source/pza/interfaces/bps_chan_ctrl.hxx +++ b/source/pza/interfaces/bps_chan_ctrl.hxx @@ -11,7 +11,7 @@ namespace pza { class device; - class bps_chan_ctrl : public interface + class bps_chan_ctrl : public itface { public: using ptr = std::shared_ptr; diff --git a/source/pza/interfaces/meter.cxx b/source/pza/interfaces/meter.cxx index cb25e3c..1921899 100644 --- a/source/pza/interfaces/meter.cxx +++ b/source/pza/interfaces/meter.cxx @@ -3,7 +3,7 @@ using namespace pza; meter::meter(device *device, const std::string &name) - : interface(device, name), + : itface(device, name), _measure("measure") { _measure.add_ro_field("value"); diff --git a/source/pza/interfaces/meter.hxx b/source/pza/interfaces/meter.hxx index 07894b4..6f79e15 100644 --- a/source/pza/interfaces/meter.hxx +++ b/source/pza/interfaces/meter.hxx @@ -3,7 +3,7 @@ #include namespace pza { - class meter : public interface + class meter : public itface { public: meter(device *device, const std::string &name); From 354e95ff620104ede599e09de67be0acb2e4db7c Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Tue, 3 Oct 2023 00:09:22 +0200 Subject: [PATCH 04/11] [PZACXX-44] new build.sh and install_dependencies.sh scripts --- scripts/build.sh | 173 ++++++++++++++------------------ scripts/install_dependencies.sh | 85 ++++++++++++++++ 2 files changed, 161 insertions(+), 97 deletions(-) create mode 100755 scripts/install_dependencies.sh diff --git a/scripts/build.sh b/scripts/build.sh index f0f04d4..b5f667b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,110 +1,89 @@ #!/bin/bash -BUILD_DIR_LINUX="build" -BUILD_DIR_WINDOWS="buildwin" -BUILD_TYPE="Debug" -BUILD_EXAMPLES="False" -SHARED="True" TARGET="Linux" -BUILD_DIR=$BUILD_DIR_LINUX +LIB_MODE="Shared" +BUILD_MODE="Debug" +PROJECT_ROOT_DIR="$(dirname "$(realpath "$0")")/.." +CONAN_PROFILES_DIR="$PROJECT_ROOT_DIR/conan_profiles" -function set_build_dir { - if [ "$TARGET" = "Windows" ]; then - BUILD_DIR=$BUILD_DIR_WINDOWS - fi - if [ "$SHARED" = "False" ]; then - BUILD_DIR="$BUILD_DIR-static" - fi -} - -function install_deps { - echo "Installing dependencies for $TARGET..." - if [ "$TARGET" = "Windows" ]; then - EXTRA_CONAN_ARGS="-pr:h ../conan_profiles/x86_64_Cross_Windows" - else - EXTRA_CONAN_ARGS="-pr:h ../conan_profiles/x86_64_Linux" - fi - mkdir -p $BUILD_DIR - cd $BUILD_DIR - conan install .. -o shared=$SHARED -o build_examples=$BUILD_EXAMPLES -s build_type=$BUILD_TYPE --build=missing -pr:b ../conan_profiles/x86_64_Linux $EXTRA_CONAN_ARGS -} - -function build { - echo "Building for $TARGET..." - cd $BUILD_DIR - conan build .. -} - -function clean { - echo "Cleaning build directory for $TARGET" - set_build_dir - rm -rf $BUILD_DIR -} +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 -function usage { - echo "Usage: $0 [-t ] [-r] [-s] [-d] [-e] [-c] [-h]" - echo " -t Target platform (Windows or Linux). Default is Linux" - echo " -r Build in Release mode. Default is Debug" - echo " -s Build in Static mode. Default is Shared" - echo " -d Install dependencies" - echo " -e Build examples" - echo " -c Clean build directory" - echo " -h Display this help message" -} +eval set -- "$TEMP" -# Parse command line arguments -while getopts "t:rscdebh" opt; do - case $opt in - t) - TARGET=$OPTARG - if [ "$TARGET" != "Windows" ] && [ "$TARGET" != "Linux" ]; then - echo "Invalid target: $TARGET" >&2 - usage - exit 1 - fi - ;; - r) - BUILD_TYPE="Release" - ;; - s) - SHARED="False" - ;; - c) - CLEAN="True" - ;; - d) - DEPS="True" - ;; - e) - BUILD_EXAMPLES="True" - ;; - h) - usage - exit 0 - ;; - \?) - echo "Invalid option: -$OPTARG" >&2 - usage - exit 1 - ;; - :) - echo "Option -$OPTARG requires an argument." >&2 - usage - exit 1 - ;; - esac +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 -set_build_dir +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 [ "$CLEAN" = "True" ]; then - clean - exit 0 +if [ "$LIB_MODE" == "Static" ]; then + BUILD_DIR="${BUILD_DIR}_static" +elif [ "$LIB_MODE" == "Shared" ]; then + pass +else + echo "Library mode not supported: $LIB_MODE" + exit 1 fi -if [ "$DEPS" = "True" ]; then - install_deps - exit 0 +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 -build \ No newline at end of file +cd $FULL_BUILD_DIR +cmake -DCMAKE_TOOLCHAIN_FILE=./$BUILD_MODE/generators/pzacxx_toolchain.cmake -DCMAKE_BUILD_TYPE=$BUILD_MODE .. +cmake --build . --config $BUILD_MODE --parallel ${nproc} \ No newline at end of file diff --git a/scripts/install_dependencies.sh b/scripts/install_dependencies.sh new file mode 100755 index 0000000..38c4f4d --- /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 \ No newline at end of file From 05619c1f347720a9c8a95e569d588bd1ea3c4f10 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Tue, 3 Oct 2023 00:30:18 +0200 Subject: [PATCH 05/11] [PZACXX-20] Fixed BPS example --- conanfile.py | 2 +- examples/bps.cxx | 55 ++++++++++++++------------------- scripts/build.sh | 17 +++++++--- scripts/install_dependencies.sh | 2 +- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/conanfile.py b/conanfile.py index 4e0507d..060be8d 100644 --- a/conanfile.py +++ b/conanfile.py @@ -12,7 +12,7 @@ class PzaCxx(ConanFile): } default_options = { "shared": True, - "build_examples": False + "build_examples": True } generators = "CMakeDeps", "CMakeToolchain", "virtualrunenv" exports_sources = "CMakeLists.txt", "source/*", "version.h.in", "CHANGELOG.md", "test/*", "cmake/*", "examples/*", "LICENSE" diff --git a/examples/bps.cxx b/examples/bps.cxx index d4145c2..4a84205 100644 --- a/examples/bps.cxx +++ b/examples/bps.cxx @@ -1,47 +1,40 @@ #include -#include #include -int main(void) +int main(int argc, char** argv) { - pza::core::set_log_level(pza::core::log_level::trace); + 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("localhost", 1883); + pza::client::ptr cli = std::make_shared(address, port); - if (cli->connect() == -1) { + if (cli->connect() == -1) return -1; - } - pza::bps::ptr bps = std::make_shared("default", "MY BPS 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 < 1; i++) { + 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()); -// bps_channel->ctrl.set_enable(true); -// bps_channel->ctrl.set_voltage(3.3); -// bps_channel->ctrl.set_current(5.0); -// spdlog::info(" Voltage: {}", bps_channel->voltmeter.get_measure()); -// spdlog::info(" Current: {}", bps_channel->ampermeter.get_measure()); -// spdlog::info(" Enabled: {}", bps_channel->ctrl.get_enable()); - - //bps_channel->voltmeter.set_measure_polling_cycle(2); - //bps_channel->ampermeter.set_measure_polling_cycle(2); - //bps_channel->ctrl.set_enable_polling_cycle(2); + 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()); } - spdlog::info("\n\nOK\n\n"); - - cli->disconnect(); - - return 0; + return cli->disconnect(); } diff --git a/scripts/build.sh b/scripts/build.sh index b5f667b..c750123 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -5,8 +5,9 @@ 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 -n "$0" -- "$@") +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 @@ -28,6 +29,10 @@ while true; do 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" @@ -63,7 +68,7 @@ fi if [ "$LIB_MODE" == "Static" ]; then BUILD_DIR="${BUILD_DIR}_static" elif [ "$LIB_MODE" == "Shared" ]; then - pass + continue else echo "Library mode not supported: $LIB_MODE" exit 1 @@ -85,5 +90,9 @@ if [ ! -d "$FULL_BUILD_DIR" ]; then fi cd $FULL_BUILD_DIR -cmake -DCMAKE_TOOLCHAIN_FILE=./$BUILD_MODE/generators/pzacxx_toolchain.cmake -DCMAKE_BUILD_TYPE=$BUILD_MODE .. -cmake --build . --config $BUILD_MODE --parallel ${nproc} \ No newline at end of file +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/install_dependencies.sh b/scripts/install_dependencies.sh index 38c4f4d..ad273dc 100755 --- a/scripts/install_dependencies.sh +++ b/scripts/install_dependencies.sh @@ -82,4 +82,4 @@ conan install \ --profile:b $FULL_PROFILE_BUILD \ --profile:h $FULL_PROFILE_HOST \ --install-folder=$FULL_BUILD_DIR \ - $PROJECT_ROOT_DIR \ No newline at end of file + $PROJECT_ROOT_DIR From 0864374fff98ec4991e81b6a3720edcd078f3d46 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Thu, 5 Oct 2023 00:06:56 +0200 Subject: [PATCH 06/11] PZACXX-5 - Docker registry done --- CMakeLists.txt | 3 +- Dockerfile | 12 -------- docker.sh | 29 ------------------- docker/Dockerfile | 33 ++++++++++++++++++++++ examples/CMakeLists.txt | 2 +- run.sh | 12 -------- scripts/build.sh | 4 +-- scripts/docker_pull.sh | 3 ++ scripts/docker_registry.sh | 58 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 98 insertions(+), 58 deletions(-) delete mode 100644 Dockerfile delete mode 100755 docker.sh create mode 100644 docker/Dockerfile delete mode 100755 run.sh create mode 100755 scripts/docker_pull.sh create mode 100755 scripts/docker_registry.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 96bcc75..458df0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,9 +59,10 @@ target_link_libraries(${LIBRARY_NAME} ) 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}/bin/*.dll + ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin/*.dll $ ) endif() diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 758c146..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM pza-cpp-img:latest - -ARG USER_ID -ARG GROUP_ID - -RUN groupadd -g $GROUP_ID dummy -RUN useradd -m -u $USER_ID -g $GROUP_ID -s /bin/bash dummy -RUN echo 'dummy:dummy' | chpasswd - -USER dummy - -WORKDIR /work \ No newline at end of file diff --git a/docker.sh b/docker.sh deleted file mode 100755 index 9539ac4..0000000 --- a/docker.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -CPP_IMG_DEP="pza-cpp-img" -IMAGE_NAME="pza-libcxx-img" -CONTAINER_NAME="pza-libcxx" - -IMAGE=$(docker images -q $CPP_IMG_DEP) -if [ -z "$IMAGE" ]; then - echo "Docker image $CPP_IMG_DEP not found. You need to build the $CPP_IMG_DEP image first." - exit 1 -fi - -# Check if the Docker image exists -IMAGE=$(docker images -q $IMAGE_NAME) -if [ -z "$IMAGE" ]; then - echo "Docker image not found. Building image from Dockerfile..." - docker build --no-cache --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) -t $IMAGE_NAME . -fi - -# Check if the Docker container exists -CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME) -if [ -z "$CONTAINER" ]; then - # If the container does not exist, create and start it - echo "Docker container not found. Creating and starting container..." - docker run --rm -v $(pwd):/work -di --name $CONTAINER_NAME $IMAGE_NAME - fi - -echo "Running docker exec on the container..." -docker exec -it $CONTAINER_NAME bash -c "/work/scripts/build.sh $*" 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 index f4150bf..4703985 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -11,7 +11,7 @@ foreach(example ${examples}) 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}/bin/*.dll + ${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin/*.dll $ ) endif() diff --git a/run.sh b/run.sh deleted file mode 100755 index 0c385a9..0000000 --- a/run.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Check if the Docker container exists -CONTAINER=$(docker ps -aq -f name=pza-libcxx) -if [ -z "$CONTAINER" ]; then - # If the container does not exist, create and start it - echo "Docker container not found. Running container..." - docker run --rm --network=host -v $(pwd):/work -di --name pza-libcxx pza-cpp-img - fi - -echo "Running docker exec on the container..." -docker exec -it pza-libcxx bash -c "/work/$*" diff --git a/scripts/build.sh b/scripts/build.sh index c750123..f6fc9d4 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -67,9 +67,7 @@ fi if [ "$LIB_MODE" == "Static" ]; then BUILD_DIR="${BUILD_DIR}_static" -elif [ "$LIB_MODE" == "Shared" ]; then - continue -else +elif [ "$LIB_MODE" != "Shared" ]; then echo "Library mode not supported: $LIB_MODE" exit 1 fi 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 From 7ed47fa07198add63d354dd88f9118aa18908742 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Sun, 15 Oct 2023 15:02:25 +0200 Subject: [PATCH 07/11] Fixed mismatch if family is uppercase --- source/pza/core/device_factory.cxx | 13 ++++++++----- source/pza/core/device_factory.hxx | 1 - 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/source/pza/core/device_factory.cxx b/source/pza/core/device_factory.cxx index 82b6776..81ce6ab 100644 --- a/source/pza/core/device_factory.cxx +++ b/source/pza/core/device_factory.cxx @@ -2,14 +2,17 @@ using namespace pza; -std::map device_factory::_factory_map = { - { "bps", device_factory::allocate_device } -}; device::ptr device_factory::create_device(const std::string &family, const std::string &group, const std::string &name) { - auto it = _factory_map.find(family); - if (it == _factory_map.end()) { + 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; } diff --git a/source/pza/core/device_factory.hxx b/source/pza/core/device_factory.hxx index 2a036c9..f877115 100644 --- a/source/pza/core/device_factory.hxx +++ b/source/pza/core/device_factory.hxx @@ -25,6 +25,5 @@ namespace pza private: using factory_function = std::function; - static std::map _factory_map; }; }; \ No newline at end of file From 76bd1eb5cd88dc7be803477cc67c86eeae305526 Mon Sep 17 00:00:00 2001 From: Antoine Gouby Date: Fri, 27 Oct 2023 15:52:54 +0200 Subject: [PATCH 08/11] Fix missing unsubscribe causing segv --- source/pza/core/client.cxx | 2 ++ source/pza/core/device.cxx | 1 + 2 files changed, 3 insertions(+) diff --git a/source/pza/core/client.cxx b/source/pza/core/client.cxx index 4095c79..1286c07 100644 --- a/source/pza/core/client.cxx +++ b/source/pza/core/client.cxx @@ -359,6 +359,8 @@ device::ptr client::create_device(const std::string &topic_str) 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; diff --git a/source/pza/core/device.cxx b/source/pza/core/device.cxx index 0de2bf2..7e8422e 100644 --- a/source/pza/core/device.cxx +++ b/source/pza/core/device.cxx @@ -19,6 +19,7 @@ void device::reset() _manufacturer = ""; } + int device::_set_identity(const std::string &payload) { std::string family; From 1643eebc8ad53d224c989855c30336016ca124cb Mon Sep 17 00:00:00 2001 From: agouby Date: Tue, 31 Oct 2023 12:49:20 +0100 Subject: [PATCH 09/11] Updated README.md for v0.1.0 release --- README.md | 56 +++++++++++++++++++------------------------------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 516dc40..308ebba 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ User library to develop C++ applications following Panduza API. ### Build Deps -| Package | Version | -| ------- | ------- | -| conan | 1.58 | -| cmake | 3.25.0 | +| Package | Version | +| ------- | -------- | +| GCC | 13 | +| conan | 1.60 | +| cmake | >=3.25.0 | Library dependencies are managed wih Conan. @@ -25,45 +26,26 @@ To install conan, https://conan.io/downloads.html. | Google test | cci.20210126 | | | cppcheck | 2.10 | | -## Version - -The version of the library is fetched from the top line of CHANGELOG.md. - ## Build -To build the library in Debug or Release mode: - ``` -mkdir build -cd build -conan install .. --build=missing -s build_type= -cmake -DCMAKE_INSTALL_PREFIX=./install -DCMAKE_BUILD_TYPE= -DCMAKE_TOOLCHAIN_FILE=.//generators/conan_toolchain.cmake .. -cmake --build . -cmake --install . +./scripts/install_dependencies.sh +./scripts/build.sh ``` -To build and export the library as a conan package: - ``` -mkdir build -cd build -conan create .. --build=missing -s build_type= +$ ./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 ``` - -## Test - -After building the library, you can test it with: - -``` -cd build -make test ``` - -## Check - -You can use cppcheck to examine the code. - +$ ./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 ``` -cd build -make check -``` \ No newline at end of file From 4355699ac1608f812dc1c28ec661e675fc3586b7 Mon Sep 17 00:00:00 2001 From: Xavier RODRIGUEZ Date: Tue, 31 Oct 2023 15:32:16 +0100 Subject: [PATCH 10/11] Pzacxx 65 update scan mecanism with specification 0.2.0 (#3) * update scan mecanism (partial/need some precision on the spec + platform fix) * Refactor to match current specification * fix scan function and remove one ugly fix * remove dirty hack 2 * Proposal to remove arbitrary wait --------- Co-authored-by: Rodriguez Co-authored-by: Antoine Gouby --- source/pza/core/client.cxx | 101 ++++++++++++++++++------ source/pza/core/client.hxx | 6 +- source/pza/core/grouped_interface.hxx | 2 +- source/pza/core/interface.cxx | 2 +- source/pza/interfaces/bps_chan_ctrl.cxx | 34 ++++---- source/pza/interfaces/bps_chan_ctrl.hxx | 4 +- 6 files changed, 101 insertions(+), 48 deletions(-) diff --git a/source/pza/core/client.cxx b/source/pza/core/client.cxx index 1286c07..4ddb4de 100644 --- a/source/pza/core/client.cxx +++ b/source/pza/core/client.cxx @@ -43,7 +43,7 @@ int client::connect(void) spdlog::error("failed to connect to client: {}", exc.what()); return -1; } - ret = scan_devices(); + ret = scan(); if (ret == 0) spdlog::info("connected to {}", _addr); else { @@ -73,6 +73,15 @@ 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; @@ -176,35 +185,44 @@ void client::message_arrived(mqtt::const_message_ptr msg) } } -int client::scan_devices(void) +// ============================================================================ +// +int client::_scan_platforms(void) { bool ret; std::condition_variable cv; std::unique_lock lock(_mtx); - if (is_connected() == false) { - spdlog::error("scan failed. Not connected to {}", _addr); - return -1; - } - + // Reset result variables _scan_device_count_expected = 0; _scan_device_results.clear(); - spdlog::debug("scanning for devices on {}...", _addr); - - _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(); - }); + // 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; @@ -213,32 +231,65 @@ int client::scan_devices(void) cv.notify_all(); }); - _publish("pza", "*"); + // 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"); - _unsubscribe("pza/server/+/+/atts/info"); + // Process timeout error if (ret == false) { - spdlog::error("timed out waiting for scan results"); + 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; @@ -261,8 +312,9 @@ int client::_scan_interfaces(std::unique_lock &lock, const device::p cv.notify_all(); }); - _publish(device->_get_device_topic(), "*"); - + // 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())); }); @@ -271,7 +323,7 @@ int client::_scan_interfaces(std::unique_lock &lock, const device::p if (ret == false) { spdlog::error("timed out waiting for scan results"); - spdlog::trace("_scan_itf_count_expected = {}, got = {}", _scan_itf_count_expected, _scan_itf_results.size()); + spdlog::error("_scan_itf_count_expected = {}, got = {}", _scan_itf_count_expected, _scan_itf_results.size()); return -1; } @@ -282,7 +334,6 @@ int client::_scan_interfaces(std::unique_lock &lock, const device::p spdlog::trace("interface: {}", it.first); } } - return 0; } diff --git a/source/pza/core/client.hxx b/source/pza/core/client.hxx index 6c51f47..ed340fa 100644 --- a/source/pza/core/client.hxx +++ b/source/pza/core/client.hxx @@ -31,7 +31,7 @@ namespace pza int connect(void); int disconnect(void); - int scan_devices(void); + int scan(void); bool is_connected(void) const { return (_paho_client->is_connected()); } void set_scan_timeout(unsigned int timeout) { _scan_timeout = timeout; } @@ -82,7 +82,9 @@ namespace pza 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_interfaces(std::unique_lock &lock, const device::ptr &device); + 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/grouped_interface.hxx b/source/pza/core/grouped_interface.hxx index df36b8f..3de5089 100644 --- a/source/pza/core/grouped_interface.hxx +++ b/source/pza/core/grouped_interface.hxx @@ -42,7 +42,7 @@ namespace pza 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) + ":")); + channels.push_back(std::make_shared(device, ":" + name + "_" + std::to_string(i) + ":_")); } return ret; diff --git a/source/pza/core/interface.cxx b/source/pza/core/interface.cxx index d09850d..9a89c0b 100644 --- a/source/pza/core/interface.cxx +++ b/source/pza/core/interface.cxx @@ -30,7 +30,7 @@ void itface::register_attribute(attribute &attribute) }); if (cv.wait_for(lock, std::chrono::seconds(5), [&]() { return received; }) == false) { - spdlog::error("timed out waiting for attribute registration"); + spdlog::error("timed out waiting for attribute registration ({})", topic); } _device->get_client()->_unsubscribe(topic); if (received) { diff --git a/source/pza/interfaces/bps_chan_ctrl.cxx b/source/pza/interfaces/bps_chan_ctrl.cxx index 7732439..e36ec51 100644 --- a/source/pza/interfaces/bps_chan_ctrl.cxx +++ b/source/pza/interfaces/bps_chan_ctrl.cxx @@ -6,50 +6,50 @@ using namespace pza; bps_chan_ctrl::bps_chan_ctrl(device *device, const std::string &name) : itface(device, name), - _volts("volts"), - _amps("amps"), + _att_voltage("voltage"), + _att_current("current"), _enable("enable") { - _volts.add_rw_field("goal"); - _volts.add_ro_field("min"); - _volts.add_ro_field("max"); - _volts.add_ro_field("decimals"); + _att_voltage.add_rw_field("goal"); + _att_voltage.add_ro_field("min"); + _att_voltage.add_ro_field("max"); + _att_voltage.add_ro_field("decimals"); - _amps.add_rw_field("goal"); - _amps.add_ro_field("min"); - _amps.add_ro_field("max"); - _amps.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({&_volts, &_amps, &_enable}); + register_attributes({&_att_voltage, &_att_current, &_enable}); } int bps_chan_ctrl::set_voltage(double volts) { - double min = _volts.get_field("min").get(); - double max = _volts.get_field("max").get(); + 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 _volts.get_field("goal").set(volts); + return _att_voltage.get_field("goal").set(volts); } int bps_chan_ctrl::set_current(double amps) { - double min = _amps.get_field("min").get(); - double max = _amps.get_field("max").get(); + 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 _amps.get_field("goal").set(amps); + return _att_current.get_field("goal").set(amps); } int bps_chan_ctrl::set_enable(bool enable) diff --git a/source/pza/interfaces/bps_chan_ctrl.hxx b/source/pza/interfaces/bps_chan_ctrl.hxx index d5346f3..a1b0cde 100644 --- a/source/pza/interfaces/bps_chan_ctrl.hxx +++ b/source/pza/interfaces/bps_chan_ctrl.hxx @@ -28,8 +28,8 @@ namespace pza void remove_enable_callback(const std::function &callback); private: - attribute _volts; - attribute _amps; + attribute _att_voltage; + attribute _att_current; attribute _enable; }; }; \ No newline at end of file From bff05f158b09bf651c7b3fd3e93a5395fe73d1ca Mon Sep 17 00:00:00 2001 From: agouby Date: Tue, 7 Nov 2023 19:18:14 +0100 Subject: [PATCH 11/11] Changed goal to value for bps --- source/pza/interfaces/bps_chan_ctrl.cxx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/pza/interfaces/bps_chan_ctrl.cxx b/source/pza/interfaces/bps_chan_ctrl.cxx index e36ec51..8811309 100644 --- a/source/pza/interfaces/bps_chan_ctrl.cxx +++ b/source/pza/interfaces/bps_chan_ctrl.cxx @@ -10,12 +10,12 @@ bps_chan_ctrl::bps_chan_ctrl(device *device, const std::string &name) _att_current("current"), _enable("enable") { - _att_voltage.add_rw_field("goal"); + _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("goal"); + _att_current.add_rw_field("value"); _att_current.add_ro_field("min"); _att_current.add_ro_field("max"); _att_current.add_ro_field("decimals"); @@ -36,7 +36,7 @@ int bps_chan_ctrl::set_voltage(double volts) return -1; } - return _att_voltage.get_field("goal").set(volts); + return _att_voltage.get_field("value").set(volts); } int bps_chan_ctrl::set_current(double amps) @@ -49,7 +49,7 @@ int bps_chan_ctrl::set_current(double amps) return -1; } - return _att_current.get_field("goal").set(amps); + return _att_current.get_field("value").set(amps); } int bps_chan_ctrl::set_enable(bool enable) @@ -75,4 +75,4 @@ void bps_chan_ctrl::add_enable_callback(const std::function &callbac 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 +}