From 60daee2a7356beccb17ce143611273c603198117 Mon Sep 17 00:00:00 2001 From: Jaka Mohorko Date: Wed, 22 Jan 2025 13:32:50 +0100 Subject: [PATCH] Initial commit --- .clang-format | 62 +++++ .editorconfig | 15 ++ .gitattributes | 3 + .gitignore | 46 ++++ CMakeLists.txt | 60 +++++ README.md | 9 + cmake/CommonUtils.cmake | 151 +++++++++++ example_application/CMakeLists.txt | 10 + example_application/main.cpp | 26 ++ example_module/CMakeLists.txt | 8 + .../include/example_module/common.h | 24 ++ .../include/example_module/dispatch.h | 56 ++++ .../include/example_module/example_fb.h | 77 ++++++ .../include/example_module/example_module.h | 32 +++ .../include/example_module/module_dll.h | 20 ++ example_module/src/CMakeLists.txt | 45 ++++ example_module/src/example_fb.cpp | 251 ++++++++++++++++++ example_module/src/example_module.cpp | 42 +++ example_module/src/module_dll.cpp | 9 + example_module/tests/CMakeLists.txt | 18 ++ example_module/tests/test_app.cpp | 21 ++ example_module/tests/test_example_module.cpp | 77 ++++++ external/CMakeLists.txt | 10 + external/openDAQ/CMakeLists.txt | 12 + 24 files changed, 1084 insertions(+) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 cmake/CommonUtils.cmake create mode 100644 example_application/CMakeLists.txt create mode 100644 example_application/main.cpp create mode 100644 example_module/CMakeLists.txt create mode 100644 example_module/include/example_module/common.h create mode 100644 example_module/include/example_module/dispatch.h create mode 100644 example_module/include/example_module/example_fb.h create mode 100644 example_module/include/example_module/example_module.h create mode 100644 example_module/include/example_module/module_dll.h create mode 100644 example_module/src/CMakeLists.txt create mode 100644 example_module/src/example_fb.cpp create mode 100644 example_module/src/example_module.cpp create mode 100644 example_module/src/module_dll.cpp create mode 100644 example_module/tests/CMakeLists.txt create mode 100644 example_module/tests/test_app.cpp create mode 100644 example_module/tests/test_example_module.cpp create mode 100644 external/CMakeLists.txt create mode 100644 external/openDAQ/CMakeLists.txt diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..4ddfb11 --- /dev/null +++ b/.clang-format @@ -0,0 +1,62 @@ +--- +BasedOnStyle: Chromium +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +#AllowAllArgumentsOnNextLine: 'false' +#AlignConsecutiveAssignments: None +#AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Left +AlignOperands: true +AllowAllParametersOfDeclarationOnNextLine: true +#AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +#AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Allman +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 140 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +IndentCaseLabels: true +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +Language: Cpp +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +ObjCBlockIndentWidth: 4 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +#SortIncludes: Never +SpaceAfterCStyleCast: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpacesBeforeTrailingComments: 2 +Standard: Auto +TabWidth: 4 +UseTab: Never +#NamespaceMacros: +# - DECLARE_TEMPLATED_OPENDAQ_INTERFACE_T +# - DECLARE_TEMPLATED_OPENDAQ_INTERFACE_T_U +# - DECLARE_OPENDAQ_INTERFACE_EX +# - DECLARE_OPENDAQ_INTERFACE +FixNamespaceComments: false +#MacroBlockBegin: "^BEGIN_NAMESPACE_OPENDAQ$" +#MacroBlockEnd: "^END_NAMESPACE_OPENDAQ" +#IndentPPDirectives: BeforeHash +#SeparateDefinitionBlocks: Always +... diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..72081c9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[{*.{cpp,h},CMakeLists.txt,*.rtclass,*.cmake,*.json}] +indent_style = space +indent_size = 4 +tab_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.yml] +ident_style = space +ident_size = 2 +tab_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8884b5b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +*.[tT][xX][tT] text +*.[sS][hH] text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..295355a --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# file types +*.bin +*.bak +*.cache +*.check_cache +*.db +*.dcu +*.depend +*.exp +*.filters +*.idb +*.ilk +*.list +*.log +*.obj +*.orig +*.pdb +*.pyc +*.rule +*.rsm +*.stamp +*.stat +*.suo +*.tlog +*.user + +# IDE files +.idea/ +.idea_/ +.vs/ +.vscode/ +__history/ +__recovery/ +__pycache__/ + +# build and backup folders +/_build* +/build* +/build_win +/cmake-build* +/out* +bckp + +# cmake +CMakeUserPresets.json +CMakeSettings.json \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6553f09 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.25) +set(REPO_NAME example_fb_module) +set(REPO_OPTION_PREFIX EXAMPLE_MODULE) + +project(${REPO_NAME} VERSION 1.0.0) + +if (POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() + +if (POLICY CMP0077) + cmake_policy(SET CMP0077 NEW) +endif() + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +list(APPEND CMAKE_MESSAGE_CONTEXT ${REPO_NAME}) +set(CMAKE_MESSAGE_CONTEXT_SHOW ON CACHE BOOL "Show CMake message context") + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +add_definitions(-DFMT_HEADER_ONLY) + +option(OPENDAQ_FB_EXAMPLE_ENABLE_APP "Enable building example function block application" OFF) +option(EXAMPLE_MODULE_ENABLE_TESTS "Enable building of test suite for the example function block module" OFF) + +include(CommonUtils) +setup_repo(${REPO_OPTION_PREFIX}) + +if(OPENDAQ_FB_EXAMPLE_ENABLE_APP) + set(DAQMODULES_REF_FB_MODULE ON CACHE BOOL "" FORCE) + set(DAQMODULES_REF_DEVICE_MODULE ON CACHE BOOL "" FORCE) +endif() + +add_subdirectory(external) +add_subdirectory(example_module) + +if(OPENDAQ_FB_EXAMPLE_ENABLE_APP) + message(STATUS "Enabled example function block application") + add_subdirectory(example_application) +endif() + +# Set CPack variables +set(CPACK_COMPONENTS_ALL RUNTIME) +set(CPACK_PROJECT_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) +set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_BINARY_DIR}/package") + +# Set the CPack generator based on the platform +if (WIN32) + set(CPACK_GENERATOR "ZIP") +elseif (UNIX AND NOT APPLE) + cmake_host_system_information(RESULT DISTRO_ID QUERY DISTRIB_ID) + cmake_host_system_information(RESULT DISTRO_VERSION_ID QUERY DISTRIB_VERSION_ID) + set(CPACK_SYSTEM_NAME "${DISTRO_ID}${DISTRO_VERSION_ID}") + set(CPACK_GENERATOR "TGZ") +endif() + +# Include CPack for packaging +include(CPack) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f46fd5b --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Example function block module + +Simple example that builds an openDAQ module giving access to an example function block. Said function block scales an input signal with a provided scale, and offsets it by a provided offset. + +## Testing the module + +To test the module, enable the `OPENDAQ_FB_EXAMPLE_ENABLE_APP` cmake flag. Doing so will add an the openDAQ reference device and function block modules to your project. Those are used to create a simulator device via the "daqref://device0" connection string, as well as a renderer via the "RefFBModuleRenderer" function block ID. The main application connects a reference device signal into both the example scaler and renderer. Additionally, it connects the scaler output into the renderer. + +To add additional tests, enable the `EXAMPLE_MODULE_ENABLE_TESTS` cmake flag. Doing so will create a new test target configured for use with the GTest framework. \ No newline at end of file diff --git a/cmake/CommonUtils.cmake b/cmake/CommonUtils.cmake new file mode 100644 index 0000000..574b366 --- /dev/null +++ b/cmake/CommonUtils.cmake @@ -0,0 +1,151 @@ +macro(setup_repo REPO_OPTION_PREFIX) + if (NOT DEFINED PROJECT_SOURCE_DIR) + message(FATAL_ERROR "Must be run inside a project()") + endif() + + # Additional build options + option(${REPO_OPTION_PREFIX}_DISABLE_DEBUG_POSTFIX "Disable debug ('-debug') postfix" OFF) + option(${REPO_OPTION_PREFIX}_DEBUG_WARNINGS_AS_ERRORS "Treat debug warnings as errors" OFF) + option(${REPO_OPTION_PREFIX}_ENABLE_TESTS "Enable unit-tests for ${REPO_OPTION_PREFIX}" ON) + + get_filename_component(ROOT_DIR ${CMAKE_SOURCE_DIR} REALPATH) + + if (NOT ${PROJECT_SOURCE_DIR} STREQUAL ${ROOT_DIR}) + set(BUILDING_AS_SUBMODULE ON PARENT_SCOPE) + message(STATUS "Building as submodule") + else() + message(STATUS "Building standalone") + set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER ".CMakePredefinedTargets") + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + + message(STATUS "Platform: ${CMAKE_SYSTEM_PROCESSOR} | ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_VERSION}") + message(STATUS "Generator: ${CMAKE_GENERATOR} | ${CMAKE_GENERATOR_PLATFORM}") + + if (IS_MULTICONFIG) + message(STATUS "Configuration types:") + + block() + list(APPEND CMAKE_MESSAGE_INDENT "\t") + + foreach(CONFIG_TYPE ${CMAKE_CONFIGURATION_TYPES}) + message(STATUS ${CONFIG_TYPE}) + endforeach() + endblock() + else() + message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") + endif() + + string(TIMESTAMP CONFIGURE_DATE) + string(TIMESTAMP CURRENT_YEAR "%Y") + endif() + + set(CMAKE_CXX_STANDARD 17) + if (WIN32) + add_compile_definitions(NOMINMAX + _WIN32_WINNT=0x0601 # Windows 7 Compat + ) + + add_compile_definitions(UNICODE _UNICODE) + endif() + + if(NOT CMAKE_DEBUG_POSTFIX AND NOT ${REPO_OPTION_PREFIX}_DISABLE_DEBUG_POSTFIX) + set(CMAKE_DEBUG_POSTFIX -debug) + endif() + + if (MSVC) + # As above CMAKE_CXX_STANDARD but for VS + add_compile_options($<$:/std:c++17>) + + foreach (flag IN ITEMS + # Set source and execution character sets to UTF-8 + # https://learn.microsoft.com/en-us/cpp/build/reference/utf-8-set-source-and-executable-character-sets-to-utf-8 + /utf-8 + # Display level 1, level 2, and level 3 warnings, and all level 4 (informational) warnings that aren't off by default. + # https://learn.microsoft.com/en-us/cpp/build/reference/compiler-option-warning-level + /W4 + # data member 'member1' will be initialized after data member 'member2' + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/c5038 + #/w15038 + # Supress warnings + # https://learn.microsoft.com/en-us/cpp/build/reference/compiler-option-warning-level + # + # 'class1' : inherits 'class2::member' via dominance + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4250 + #/wd4250 + # Your code uses a function, class member, variable, or typedef that's marked deprecated. + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4996 + /wd4996 + # declaration of 'identifier' hides class member + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4458 + /wd4458 + # nonstandard extension used : nameless struct/union + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4201 + #/wd4201 + # unreachable code + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4702 + #/wd4702 + # declaration of 'identifier' hides global declaration + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4459 + #/wd4459 + # 'function' : unreferenced local function has been removed + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4505 + #/wd4505 + # conditional expression is constant + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4127 + #/wd4127 + # assignment within conditional expression + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4706 + #/wd4706 + # loss of data / precision, unsigned <--> signed + # + # 'argument' : conversion from 'type1' to 'type2', possible loss of data + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4244 + /wd4244 + # 'var' : conversion from 'size_t' to 'type', possible loss of data + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4267 + #/wd4267 + # 'identifier' : unreferenced formal parameter + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-4-c4100 + /wd4100 + ) + add_compile_options($<$:${flag}>) + endforeach() + + if (NOT OPENDAQ_MSVC_SINGLE_PROCESS_BUILD) + # Build with multiple processes + # https://learn.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes + add_compile_options($<$:/MP>) + endif() + + # Treat warnings as errors if not Debug or OPENDAQ_DEBUG_WARNINGS_AS_ERRORS is ON + add_compile_options($<$>,$>:/WX>) + + add_compile_definitions($<$:_DEBUG>) + + if (MSVC_VERSION GREATER_EQUAL 1910) + # /Zc:__cplusplus forces MSVC to use the correct value of __cplusplus macro (otherwise always C++98) + add_compile_options($<$:/Zc:__cplusplus>) + if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + # /Zf (Faster PDB generation) is not supported by ClangCL + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /Zf") + endif() + + # Produce diagnostic messages with exact location + add_compile_options($<$:/diagnostics:caret>) + endif() + + # set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /ignore:4221") + # set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /ignore:4221") + # set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} /ignore:4221") + endif() + + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + + if (${REPO_OPTION_PREFIX}_ENABLE_TESTS) + set(OPENDAQ_ENABLE_TEST_UTILS ON CACHE BOOL "Enable testing utils library") + endif() +endmacro() diff --git a/example_application/CMakeLists.txt b/example_application/CMakeLists.txt new file mode 100644 index 0000000..aeec179 --- /dev/null +++ b/example_application/CMakeLists.txt @@ -0,0 +1,10 @@ +add_executable(fb_application_example main.cpp) + +target_link_libraries(fb_application_example PRIVATE daq::opendaq) + +add_dependencies( + fb_application_example + example_module + daq::ref_device_module + daq::ref_fb_module +) \ No newline at end of file diff --git a/example_application/main.cpp b/example_application/main.cpp new file mode 100644 index 0000000..bac85ab --- /dev/null +++ b/example_application/main.cpp @@ -0,0 +1,26 @@ +/** + * Server application + */ + +#include +#include + +using namespace daq; + +int main(int /*argc*/, const char* /*argv*/[]) +{ + const auto instance = Instance(); + auto referenceDevice = instance.addDevice("daqref://device0"); + auto renderer = instance.addFunctionBlock("RefFBModuleRenderer"); + auto exampleModule = instance.addFunctionBlock("ExampleScalingModule"); + exampleModule.setPropertyValue("Scale", 3); + exampleModule.setPropertyValue("Offset", -2); + + exampleModule.getInputPorts()[0].connect(referenceDevice.getSignalsRecursive()[0]); + renderer.getInputPorts()[0].connect(referenceDevice.getSignalsRecursive()[0]); + renderer.getInputPorts()[1].connect(exampleModule.getSignals()[0]); + + std::cout << "Press \"enter\" to exit the application..." << std::endl; + std::cin.get(); + return 0; +} diff --git a/example_module/CMakeLists.txt b/example_module/CMakeLists.txt new file mode 100644 index 0000000..a5f2808 --- /dev/null +++ b/example_module/CMakeLists.txt @@ -0,0 +1,8 @@ +get_current_folder_name(TARGET_FOLDER_NAME) +project(ExampleModule VERSION 1.0.0 LANGUAGES CXX) + +add_subdirectory(src) + +if (EXAMPLE_MODULE_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/example_module/include/example_module/common.h b/example_module/include/example_module/common.h new file mode 100644 index 0000000..a1cbcd0 --- /dev/null +++ b/example_module/include/example_module/common.h @@ -0,0 +1,24 @@ +/* + * Copyright 2022-2024 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +#define BEGIN_NAMESPACE_EXAMPLE_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(example_module) + +static const std::string EXAMPLE_MODULE_NAME = "ExampleModule"; + +#define END_NAMESPACE_EXAMPLE_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/example_module/include/example_module/dispatch.h b/example_module/include/example_module/dispatch.h new file mode 100644 index 0000000..a47b7a8 --- /dev/null +++ b/example_module/include/example_module/dispatch.h @@ -0,0 +1,56 @@ +/* + * Copyright 2022-2024 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#define SAMPLE_TYPE_DISPATCH(ST, Func, ...) \ + switch (ST) \ + { \ + case SampleType::Int8: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::Int16: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::Int32: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::Int64: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::UInt8: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::UInt16: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::UInt32: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::UInt64: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::Float32: \ + Func(__VA_ARGS__); \ + break; \ + case SampleType::Float64: \ + Func(__VA_ARGS__); \ + break; \ + default: \ + assert(false); \ + } diff --git a/example_module/include/example_module/example_fb.h b/example_module/include/example_module/example_fb.h new file mode 100644 index 0000000..290385b --- /dev/null +++ b/example_module/include/example_module/example_fb.h @@ -0,0 +1,77 @@ +/* + * Copyright 2022-2024 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +BEGIN_NAMESPACE_EXAMPLE_MODULE + +class ExampleFBImpl final : public FunctionBlock +{ +public: + explicit ExampleFBImpl(const ContextPtr& ctx, const ComponentPtr& parent, const StringPtr& localId); + ~ExampleFBImpl() override = default; + + static FunctionBlockTypePtr CreateType(); + +private: + InputPortPtr inputPort; + + DataDescriptorPtr inputDataDescriptor; + DataDescriptorPtr inputDomainDataDescriptor; + + DataDescriptorPtr outputDataDescriptor; + DataDescriptorPtr outputDomainDataDescriptor; + + SampleType inputSampleType; + + SignalConfigPtr outputSignal; + SignalConfigPtr outputDomainSignal; + + StreamReaderPtr reader; + SizeT sampleRate; + std::vector inputData; + std::vector inputDomainData; + + bool configValid = false; + Float scale; + Float offset; + Float outputHighValue; + Float outputLowValue; + Bool useCustomOutputRange; + std::string outputUnit; + std::string outputName; + + void createInputPorts(); + void createSignals(); + + void calculate(); + void processData(SizeT readAmount, SizeT packetOffset) const; + void processEventPacket(const EventPacketPtr& packet); + + void processSignalDescriptorChanged(const DataDescriptorPtr& dataDescriptor, + const DataDescriptorPtr& domainDescriptor); + void configure(); + + void initProperties(); + void propertyChanged(bool configure); + void readProperties(); +}; + +END_NAMESPACE_EXAMPLE_MODULE diff --git a/example_module/include/example_module/example_module.h b/example_module/include/example_module/example_module.h new file mode 100644 index 0000000..418e48d --- /dev/null +++ b/example_module/include/example_module/example_module.h @@ -0,0 +1,32 @@ +/* + * Copyright 2022-2024 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +BEGIN_NAMESPACE_EXAMPLE_MODULE + +class ExampleModule final : public Module +{ +public: + explicit ExampleModule(ContextPtr context); + + DictPtr onGetAvailableFunctionBlockTypes() override; + FunctionBlockPtr onCreateFunctionBlock(const StringPtr& id, const ComponentPtr& parent, const StringPtr& localId, const PropertyObjectPtr& config) override; +}; + +END_NAMESPACE_EXAMPLE_MODULE diff --git a/example_module/include/example_module/module_dll.h b/example_module/include/example_module/module_dll.h new file mode 100644 index 0000000..8db6572 --- /dev/null +++ b/example_module/include/example_module/module_dll.h @@ -0,0 +1,20 @@ +/* + * Copyright 2022-2024 openDAQ d.o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +DECLARE_MODULE_EXPORTS(ExampleModule) diff --git a/example_module/src/CMakeLists.txt b/example_module/src/CMakeLists.txt new file mode 100644 index 0000000..dff37d8 --- /dev/null +++ b/example_module/src/CMakeLists.txt @@ -0,0 +1,45 @@ +set(LIB_NAME example_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + module_dll.h + example_module.h + example_fb.h +) + +set(SRC_Srcs module_dll.cpp + example_module.cpp + example_fb.cpp +) + +prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/example_module.h + ${MODULE_HEADERS_DIR}/example_fb.h + ${MODULE_HEADERS_DIR}/module_dll.h + module_dll.cpp + example_module.cpp + example_fb.cpp +) + + +add_library(${LIB_NAME} SHARED ${SRC_Include} + ${SRC_Srcs} +) + +add_library(${SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +endif() + +target_link_libraries(${LIB_NAME} PUBLIC daq::opendaq +) + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + $ +) + +opendaq_set_module_properties(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) +create_version_header(${LIB_NAME}) diff --git a/example_module/src/example_fb.cpp b/example_module/src/example_fb.cpp new file mode 100644 index 0000000..f07197d --- /dev/null +++ b/example_module/src/example_fb.cpp @@ -0,0 +1,251 @@ +#include +#include +#include + +BEGIN_NAMESPACE_EXAMPLE_MODULE + ExampleFBImpl::ExampleFBImpl(const ContextPtr& ctx, const ComponentPtr& parent, const StringPtr& localId) + : FunctionBlock(CreateType(), ctx, parent, localId) +{ + initComponentStatus(); + createInputPorts(); + createSignals(); + initProperties(); +} + +void ExampleFBImpl::initProperties() +{ + const auto scaleProp = FloatProperty("Scale", 1.0); + objPtr.addProperty(scaleProp); + objPtr.getOnPropertyValueWrite("Scale") += + [this](PropertyObjectPtr& obj, PropertyValueEventArgsPtr& args) { propertyChanged(true); }; + + const auto offsetProp = FloatProperty("Offset", 0.0); + objPtr.addProperty(offsetProp); + objPtr.getOnPropertyValueWrite("Offset") += + [this](PropertyObjectPtr& obj, PropertyValueEventArgsPtr& args) { propertyChanged(true); }; + + const auto useCustomOutputRangeProp = BoolProperty("UseCustomOutputRange", False); + objPtr.addProperty(useCustomOutputRangeProp); + objPtr.getOnPropertyValueWrite("UseCustomOutputRange") += + [this](PropertyObjectPtr& obj, PropertyValueEventArgsPtr& args) { propertyChanged(true); }; + + const auto customHighValueProp = FloatProperty("OutputHighValue", 10.0, EvalValue("$UseCustomOutputRange")); + objPtr.addProperty(customHighValueProp); + objPtr.getOnPropertyValueWrite("OutputHighValue") += + [this](PropertyObjectPtr& obj, PropertyValueEventArgsPtr& args) { propertyChanged(true); }; + + const auto customLowValueProp = FloatProperty("OutputLowValue", -10.0, EvalValue("$UseCustomOutputRange")); + objPtr.addProperty(customLowValueProp); + objPtr.getOnPropertyValueWrite("OutputLowValue") += + [this](PropertyObjectPtr& obj, PropertyValueEventArgsPtr& args) { propertyChanged(true); }; + + const auto outputNameProp = StringProperty("OutputName", ""); + objPtr.addProperty(outputNameProp); + objPtr.getOnPropertyValueWrite("OutputName") += + [this](PropertyObjectPtr& obj, PropertyValueEventArgsPtr& args) { propertyChanged(true); }; + + const auto outputUnitProp = StringProperty("OutputUnit", ""); + objPtr.addProperty(outputUnitProp); + objPtr.getOnPropertyValueWrite("OutputUnit") += + [this](PropertyObjectPtr& obj, PropertyValueEventArgsPtr& args) { propertyChanged(true); }; + + readProperties(); +} + +void ExampleFBImpl::propertyChanged(bool configure) +{ + readProperties(); + if (configure) + this->configure(); +} + +void ExampleFBImpl::readProperties() +{ + scale = objPtr.getPropertyValue("Scale"); + offset = objPtr.getPropertyValue("Offset"); + useCustomOutputRange = objPtr.getPropertyValue("UseCustomOutputRange"); + outputHighValue = objPtr.getPropertyValue("OutputHighValue"); + outputLowValue = objPtr.getPropertyValue("OutputLowValue"); + outputUnit = static_cast(objPtr.getPropertyValue("OutputUnit")); + outputName = static_cast(objPtr.getPropertyValue("OutputName")); +} + +FunctionBlockTypePtr ExampleFBImpl::CreateType() +{ + return FunctionBlockType("ExampleScalingModule", "Scaling", "Signal scaling"); +} + +void ExampleFBImpl::processSignalDescriptorChanged(const DataDescriptorPtr& dataDescriptor, + const DataDescriptorPtr& domainDescriptor) +{ + if (dataDescriptor.assigned()) + this->inputDataDescriptor = dataDescriptor; + if (domainDescriptor.assigned()) + this->inputDomainDataDescriptor = domainDescriptor; + + configure(); +} + +void ExampleFBImpl::configure() +{ + try + { + if (!inputDomainDataDescriptor.assigned() || inputDomainDataDescriptor == NullDataDescriptor()) + { + throw std::runtime_error("No domain input"); + } + + if (!inputDataDescriptor.assigned() || inputDataDescriptor == NullDataDescriptor()) + { + throw std::runtime_error("No value input"); + } + + if (inputDataDescriptor.getDimensions().getCount() > 0) + { + throw std::runtime_error("Arrays not supported"); + } + + inputSampleType = inputDataDescriptor.getSampleType(); + if (inputSampleType != SampleType::Float64 && + inputSampleType != SampleType::Float32 && + inputSampleType != SampleType::Int8 && + inputSampleType != SampleType::Int16 && + inputSampleType != SampleType::Int32 && + inputSampleType != SampleType::Int64 && + inputSampleType != SampleType::UInt8 && + inputSampleType != SampleType::UInt16 && + inputSampleType != SampleType::UInt32 && + inputSampleType != SampleType::UInt64) + { + throw std::runtime_error("Invalid sample type"); + } + + // Accept only synchronous (linear implicit) domain signals + if (inputDomainDataDescriptor.getSampleType() != SampleType::Int64 && inputDomainDataDescriptor.getSampleType() != SampleType::UInt64) + { + throw std::runtime_error("Incompatible domain data sample type"); + } + + const auto domainUnit = inputDomainDataDescriptor.getUnit(); + if (domainUnit.getSymbol() != "s" && domainUnit.getSymbol() != "seconds") + { + throw std::runtime_error("Domain unit expected in seconds"); + } + + const auto domainRule = inputDomainDataDescriptor.getRule(); + if (inputDomainDataDescriptor.getRule().getType() != DataRuleType::Linear) + { + throw std::runtime_error("Domain rule must be linear"); + } + + RangePtr outputRange; + if (useCustomOutputRange) + { + outputRange = Range(outputLowValue, outputHighValue); + } + else + { + auto outputHigh = scale * static_cast(inputDataDescriptor.getValueRange().getLowValue()) + offset; + auto outputLow = scale * static_cast(inputDataDescriptor.getValueRange().getHighValue()) + offset; + if (outputLow > outputHigh) + std::swap(outputLow, outputHigh); + + outputRange = Range(outputLow, outputHigh); + } + + auto name = outputName.empty() ? inputPort.getSignal().getName().toStdString() + "/Scaled" : outputName; + auto unit = outputUnit.empty() ? inputDataDescriptor.getUnit() : Unit(outputUnit); + + outputDataDescriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Float64) + .setValueRange(outputRange) + .setUnit(unit) + .build(); + outputDomainDataDescriptor = inputDomainDataDescriptor; + + outputSignal.setDescriptor(outputDataDescriptor); + outputSignal.setName(name); + outputDomainSignal.setDescriptor(inputDomainDataDescriptor); + + // Allocate 1s buffer + sampleRate = reader::getSampleRate(inputDomainDataDescriptor); + inputData.resize(sampleRate); + inputDomainData.resize(sampleRate); + + setComponentStatus(ComponentStatus::Ok); + } + catch (const std::exception& e) + { + setComponentStatusWithMessage(ComponentStatus::Error, fmt::format("Failed to set descriptor for output signal: {}", e.what())); + outputSignal.setDescriptor(nullptr); + configValid = false; + } + + configValid = true; +} + +void ExampleFBImpl::calculate() +{ + auto lock = this->getAcquisitionLock(); + + while (!reader.getEmpty()) + { + SizeT readAmount = std::min(reader.getAvailableCount(), sampleRate); + const auto status = reader.readWithDomain(inputData.data(), inputDomainData.data(), &readAmount); + + if (configValid) + { + processData(readAmount, status.getOffset()); + } + + if (status.getReadStatus() == ReadStatus::Event) + { + const auto eventPacket = status.getEventPacket(); + if (eventPacket.assigned()) + processEventPacket(eventPacket); + return; + } + } +} + +void ExampleFBImpl::processData(SizeT readAmount, SizeT packetOffset) const +{ + if (readAmount == 0) + return; + + const auto outputDomainPacket = DataPacket(outputDomainDataDescriptor, readAmount, packetOffset); + const auto outputPacket = DataPacketWithDomain(outputDomainPacket, outputDataDescriptor, readAmount); + auto outputData = static_cast(outputPacket.getRawData()); + + for (size_t i = 0; i < readAmount; i++) + *outputData++ = scale * static_cast(inputData[i]) + offset; + + outputSignal.sendPacket(outputPacket); + outputDomainSignal.sendPacket(outputDomainPacket); +} + +void ExampleFBImpl::processEventPacket(const EventPacketPtr& packet) +{ + if (packet.getEventId() == event_packet_id::DATA_DESCRIPTOR_CHANGED) + { + DataDescriptorPtr dataDesc = packet.getParameters().get(event_packet_param::DATA_DESCRIPTOR); + DataDescriptorPtr domainDesc = packet.getParameters().get(event_packet_param::DOMAIN_DATA_DESCRIPTOR); + processSignalDescriptorChanged(dataDesc, domainDesc); + } +} + +void ExampleFBImpl::createInputPorts() +{ + inputPort = createAndAddInputPort("Input", PacketReadyNotification::Scheduler); + reader = StreamReaderFromPort(inputPort, SampleType::Float32, SampleType::UInt64); + reader.setOnDataAvailable([this] { calculate();}); +} + +void ExampleFBImpl::createSignals() +{ + outputSignal = createAndAddSignal("Scaled"); + outputDomainSignal = createAndAddSignal("ScaledTime", nullptr, false); + outputSignal.setDomainSignal(outputDomainSignal); +} + +END_NAMESPACE_EXAMPLE_MODULE diff --git a/example_module/src/example_module.cpp b/example_module/src/example_module.cpp new file mode 100644 index 0000000..b41d01c --- /dev/null +++ b/example_module/src/example_module.cpp @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_EXAMPLE_MODULE + +ExampleModule::ExampleModule(ContextPtr context) + : Module("ReferenceFunctionBlockModule", + VersionInfo(EXAMPLE_MODULE_MAJOR_VERSION, EXAMPLE_MODULE_MINOR_VERSION, EXAMPLE_MODULE_PATCH_VERSION), + std::move(context), + "ReferenceFunctionBlockModule") +{ +} + +DictPtr ExampleModule::onGetAvailableFunctionBlockTypes() +{ + auto types = Dict(); + + const auto typeScaling = ExampleFBImpl::CreateType(); + types.set(typeScaling.getId(), typeScaling); + + return types; +} + +FunctionBlockPtr ExampleModule::onCreateFunctionBlock(const StringPtr& id, + const ComponentPtr& parent, + const StringPtr& localId, + const PropertyObjectPtr& config) +{ + if (id == ExampleFBImpl::CreateType().getId()) + { + FunctionBlockPtr fb = createWithImplementation(context, parent, localId); + return fb; + } + + LOG_W("Function block \"{}\" not found", id); + throw NotFoundException("Function block not found"); +} + +END_NAMESPACE_EXAMPLE_MODULE diff --git a/example_module/src/module_dll.cpp b/example_module/src/module_dll.cpp new file mode 100644 index 0000000..6910bc1 --- /dev/null +++ b/example_module/src/module_dll.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +using namespace daq::modules::example_module; + +DEFINE_MODULE_EXPORTS(ExampleModule) + diff --git a/example_module/tests/CMakeLists.txt b/example_module/tests/CMakeLists.txt new file mode 100644 index 0000000..7265fbc --- /dev/null +++ b/example_module/tests/CMakeLists.txt @@ -0,0 +1,18 @@ +set(MODULE_NAME example_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_example_module.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY bin +) diff --git a/example_module/tests/test_app.cpp b/example_module/tests/test_app.cpp new file mode 100644 index 0000000..4f6ea7d --- /dev/null +++ b/example_module/tests/test_app.cpp @@ -0,0 +1,21 @@ +#include +#include +#include +#include +#include + + +int main(int argc, char** args) +{ + daq::daqInitializeCoreObjectsTesting(); + daqInitModuleManagerLibrary(); + + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new DaqMemCheckListener()); + + auto res = RUN_ALL_TESTS(); + + return res; +} diff --git a/example_module/tests/test_example_module.cpp b/example_module/tests/test_example_module.cpp new file mode 100644 index 0000000..45e6032 --- /dev/null +++ b/example_module/tests/test_example_module.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include + +using namespace daq; +using ExampleModuleTest = testing::Test; + +TEST_F(ExampleModuleTest, TestAdd) +{ + const auto instance = Instance(); + ASSERT_TRUE(instance.addFunctionBlock("ExampleScalingModule").assigned()); +} + +TEST_F(ExampleModuleTest, TestPropCount) +{ + const auto instance = Instance(); + auto fb = instance.addFunctionBlock("ExampleScalingModule"); + ASSERT_EQ(fb.getAllProperties().getCount(), 7); +} + +TEST_F(ExampleModuleTest, TestDataScaling) +{ + const auto instance = Instance(); + auto fb = instance.addFunctionBlock("ExampleScalingModule"); + fb.setPropertyValue("Scale", 2); + + auto dataDescriptor = DataDescriptorBuilder().setSampleType(SampleType::Float32).setValueRange(Range(-10, 10)).build(); + auto signal = SignalWithDescriptor(instance.getContext(), dataDescriptor, nullptr, "Data"); + + const auto domainDescriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Int64) + .setUnit(Unit("s", -1, "seconds", "time")) + .setTickResolution(Ratio(1, 1000)) + .setRule(LinearDataRule(1, 0)) + .setOrigin("1970-01-01T01:00:00+00:00") + .build(); + auto domainSignal = SignalWithDescriptor(instance.getContext(), domainDescriptor, nullptr, "DomainData"); + signal.setDomainSignal(domainSignal); + + fb.getInputPorts()[0].connect(signal); + auto streamReader = StreamReader(fb.getSignals()[0], SampleType::Float32, SampleType::Int64); + + auto domainPacket = DataPacket(domainDescriptor, 10, 0); + auto packet = DataPacketWithDomain(domainPacket, dataDescriptor, 10); + float* data = static_cast(packet.getRawData()); + for (auto i = 0; i < 10; i++) + data[i] = static_cast(i); + + signal.sendPacket(packet); + domainSignal.sendPacket(domainPacket); + + SizeT count = 10; + std::vector readData; + readData.resize(count); + + auto status = streamReader.read(readData.data(), &count); + ASSERT_EQ(status.getReadStatus(), ReadStatus::Event); + ASSERT_EQ(count, 0); + + while (count < 10) + { + using namespace std::chrono_literals; + count = streamReader.getAvailableCount(); + std::this_thread::sleep_for(100ms); + } + + ASSERT_EQ(count, 10); + + status = streamReader.read(readData.data(), &count); + ASSERT_EQ(count, 10); + ASSERT_EQ(status.getReadStatus(), ReadStatus::Ok); + + for (int i = 0; i < 10; i++) + ASSERT_EQ(static_cast(readData[i]), i * 2); +} diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt new file mode 100644 index 0000000..c6ccc9d --- /dev/null +++ b/external/CMakeLists.txt @@ -0,0 +1,10 @@ +set(CMAKE_FOLDER external) +list(APPEND CMAKE_MESSAGE_CONTEXT external) + +if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) + message(FATAL_ERROR "In-source build is not supported!") +endif() + +include(FetchContent) + +add_subdirectory(openDAQ) diff --git a/external/openDAQ/CMakeLists.txt b/external/openDAQ/CMakeLists.txt new file mode 100644 index 0000000..d50be4f --- /dev/null +++ b/external/openDAQ/CMakeLists.txt @@ -0,0 +1,12 @@ +set(OPENDAQ_ENABLE_TESTS false) + +FetchContent_Declare( + openDAQ + GIT_REPOSITORY https://github.com/openDAQ/openDAQ.git + GIT_TAG origin/main + GIT_PROGRESS ON + SYSTEM + FIND_PACKAGE_ARGS 3.11.0 GLOBAL +) + +FetchContent_MakeAvailable(openDAQ)