diff --git a/CMakeLists.txt b/CMakeLists.txt index eee2515e..9c799cdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# CMakeList.txt : CMake project for OpenDAQHistorianFB, include source and define +# CMakeList.txt : CMake project for MQTTStreamingModule, include source and define # project specific logic here. # set(CMAKE_POLICY_VERSION_MINIMUM 3.5) @@ -32,6 +32,12 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") option(OPENDAQ_DEVICE_EXAMPLE_ENABLE_EXAMPLE_APPS "Enable building example applications" OFF) +if ((CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_CLANGXX) AND NOT MSVC) + if (NOT WIN32) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") + endif() +endif() + include(CommonUtils) setup_repo(${REPO_OPTION_PREFIX}) @@ -39,6 +45,8 @@ if(OPENDAQ_DEVICE_EXAMPLE_ENABLE_EXAMPLE_APPS) set(DAQMODULES_REF_DEVICE_MODULE ON CACHE BOOL "" FORCE) endif() +option(OPENDAQ_MQTT_ENABLE_TESTS "Enable module testing" OFF) +option(OPENDAQ_MQTT_ENABLE_EXAMPLE_APPS "Enable example applications building" OFF) find_package(OpenSSL REQUIRED) if (OPENSSL_FOUND) @@ -50,30 +58,9 @@ endif() add_subdirectory(external) add_subdirectory(mqtt_streaming_protocol) add_subdirectory(mqtt_streaming_server_module) +add_subdirectory(mqtt_streaming_client_module) if(OPENDAQ_DEVICE_EXAMPLE_ENABLE_EXAMPLE_APPS) message(STATUS "Example applications have been enabled") add_subdirectory(examples) 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) diff --git a/CMakePresets.json b/CMakePresets.json index 67c7e926..3fbec0a1 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -95,7 +95,9 @@ "name": "full", "hidden": true, "cacheVariables": { - "OPENDAQ_DEVICE_EXAMPLE_ENABLE_EXAMPLE_APPS": "true" + "OPENDAQ_DEVICE_EXAMPLE_ENABLE_EXAMPLE_APPS": "true", + "OPENDAQ_MQTT_ENABLE_TESTS": "true", + "OPENDAQ_MQTT_ENABLE_EXAMPLE_APPS": "true" } }, { diff --git a/README.md b/README.md index e07f2540..5c0643c3 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ cmake --build . ## Testing -There are several example applications in the *"examples"* folder. These examples are based on OpenDAQ SDK and allow testing of *SimpleMQTTModule* functional blocks with each other and with third-party MQTT tools. +There are several example applications in the *"examples"* folder. These examples are based on OpenDAQ SDK and allow testing of *MQTTStreamingModule* client/server sides with each other and with third-party MQTT tools. > ***Note:*** *Using the applications involves using a third-party broker. It must be started before example applications. See a **External MQTT tools** section for more details* @@ -57,94 +57,8 @@ There are several example applications in the *"examples"* folder. These example #### ref-dev-mqtt-pub -The *ref-dev-mqtt-pub* application is a console example which publishes *ref-device Signal* samples via the *MQTTStreamingModule* server. By default it uses the following *MQTTStreamingModule* server settings: -``` -StreamingDataPollingPeriod: 20 -MaxPacketReadCount: 1000 -BrokerAddress: "127.0.0.1" -MqttUsername: "" -MqttPassword: "" -``` - -### External MQTT tools - -It is suggested to use [***Eclipse Mosquitto***](https://github.com/eclipse-mosquitto/mosquitto) as a third-party MQTT tool set. It includes MQTT broker and MQTT publisher/subscriber clients. -Utilities could be installed on **Ubuntu**: - -```shell -sudo apt install mosquitto mosquitto-clients -``` +The *ref-dev-mqtt-pub* application is a console example which publishes *ref-device Signal* samples via the *MQTTStreamingModule* server. -The MQTT broker will be run automatically after installing. For simple testing run a subscriber with the following options: +#### ref-dev-mqtt-sub -```shell -mosquitto_sub -h 127.0.0.1 -t "openDAQ/#" -v -``` -The subscriber will wait for incoming data and then print it. Then run a publisher with the following options: -```shell -mosquitto_pub -h 127.0.0.1 -t "openDAQ/publisher" -m '{"Input0":2, "Input1":1.2, "Input3":3.3}' -``` -This command publishes a message and exits. From the subscriber's side you can see: - -```shell -user@machine:$ mosquitto_sub -h 127.0.0.1 -t "openDAQ/publisher" -v -openDAQ/publisher {"Input0":2, "Input1":1.2, "Input3":3.3} -``` -Now, you can test examples with 3rd-party tools. For example, run *ref-dev-mqtt-pub* and *mosquitto_sub* in different terminals with proper settings. -```shell -user@machine:$ ./ref-dev-mqtt-pub -[tid: 29784][2025-09-15 11:17:02.663] [ModuleManager] [info] Loaded module [v3.31.0 ReferenceDeviceModule] from "libref_device_module-64-3-debug.module.so". -[tid: 29784][2025-09-15 11:17:02.664] [ModuleManager] [info] DEV [daqref] Reference device: "Reference device" -[tid: 29784][2025-09-15 11:17:02.668] [ModuleManager] [info] Loaded module [v3.4.0 OpenDAQMqttStreamingServerModule] from "libmqtt_stream_srv_module-64-3-debug.module.so". -[tid: 29784][2025-09-15 11:17:02.670] [ModuleManager] [info] SRV [OpenDAQMQTT] openDAQ MQTT Streaming server: "Streams data over MQTT" -[tid: 29784][2025-09-15 11:17:03.196] [ReferenceDevice] [info] Properties: NumberOfChannels 2 -[tid: 29784][2025-09-15 11:17:03.201] [/RefDev1/IO/AI/RefCh0] [info] Properties: Waveform Sine, Frequency 10, DC 0, Amplitude 5, NoiseAmplitude 0, ConstantValue 2 -[tid: 29784][2025-09-15 11:17:03.201] [/RefDev1/IO/AI/RefCh0] [info] Properties: SampleRate 1000, ClientSideScaling false -[tid: 29784][2025-09-15 11:17:03.207] [/RefDev1/IO/AI/RefCh1] [info] Properties: Waveform Sine, Frequency 10, DC 0, Amplitude 5, NoiseAmplitude 0, ConstantValue 2 -[tid: 29784][2025-09-15 11:17:03.207] [/RefDev1/IO/AI/RefCh1] [info] Properties: SampleRate 1000, ClientSideScaling false -[tid: 29784][2025-09-15 11:17:03.209] [ReferenceDevice] [info] Properties: AcquisitionLoopTime 20 -[tid: 29784][2025-09-15 11:17:03.216] [Instance] [info] Root device set to daqref://device1 -[tid: 29784][2025-09-15 11:17:03.231] [ReferenceDevice] [info] Properties: NumberOfChannels 2 -[tid: 29784][2025-09-15 11:17:03.235] [/RefDev1/Dev/RefDev0/IO/AI/RefCh0] [info] Properties: Waveform Sine, Frequency 10, DC 0, Amplitude 5, NoiseAmplitude 0, ConstantValue 2 -[tid: 29784][2025-09-15 11:17:03.235] [/RefDev1/Dev/RefDev0/IO/AI/RefCh0] [info] Properties: SampleRate 1000, ClientSideScaling false -[tid: 29784][2025-09-15 11:17:03.240] [/RefDev1/Dev/RefDev0/IO/AI/RefCh1] [info] Properties: Waveform Sine, Frequency 10, DC 0, Amplitude 5, NoiseAmplitude 0, ConstantValue 2 -[tid: 29784][2025-09-15 11:17:03.240] [/RefDev1/Dev/RefDev0/IO/AI/RefCh1] [info] Properties: SampleRate 1000, ClientSideScaling false -[tid: 29784][2025-09-15 11:17:03.242] [ReferenceDevice] [info] Properties: AcquisitionLoopTime 20 -[tid: 29784][2025-09-15 11:17:03.253] [/RefDev1/Dev/RefDev0/IO/AI/ProtectedChannel] [info] Properties: Waveform Sine, Frequency 10, DC 0, Amplitude 5, NoiseAmplitude 0, ConstantValue 2 -[tid: 29784][2025-09-15 11:17:03.253] [/RefDev1/Dev/RefDev0/IO/AI/ProtectedChannel] [info] Properties: SampleRate 1000, ClientSideScaling false -[tid: 29784][2025-09-15 11:17:03.265] [OpenDAQMQTT] [info] MQTT: Trying to connect to MQTT broker (127.0.0.1) -[tid: 29784][2025-09-15 11:17:03.267] [OpenDAQMQTT] [info] Adding the Signal to reader: /RefDev1/IO/AI/RefCh0/Sig/AI0; -[tid: 29784][2025-09-15 11:17:03.268] [OpenDAQMQTT] [info] Signal /RefDev1/IO/AI/RefCh0/Sig/AI0Time doesn't has domain signal assigned, skipping -[tid: 29784][2025-09-15 11:17:03.268] [OpenDAQMQTT] [info] Adding the Signal to reader: /RefDev1/IO/AI/RefCh1/Sig/AI1; -[tid: 29784][2025-09-15 11:17:03.269] [OpenDAQMQTT] [info] Signal /RefDev1/IO/AI/RefCh1/Sig/AI1Time doesn't has domain signal assigned, skipping -[tid: 29784][2025-09-15 11:17:03.269] [OpenDAQMQTT] [info] Signal /RefDev1/Sig/Time doesn't has domain signal assigned, skipping -[tid: 29784][2025-09-15 11:17:03.269] [OpenDAQMQTT] [info] Adding the Signal to reader: /RefDev1/Dev/RefDev0/IO/AI/RefCh0/Sig/AI0; -[tid: 29784][2025-09-15 11:17:03.269] [OpenDAQMQTT] [info] Signal /RefDev1/Dev/RefDev0/IO/AI/RefCh0/Sig/AI0Time doesn't has domain signal assigned, skipping -[tid: 29784][2025-09-15 11:17:03.269] [OpenDAQMQTT] [info] Adding the Signal to reader: /RefDev1/Dev/RefDev0/IO/AI/RefCh1/Sig/AI1; -[tid: 29784][2025-09-15 11:17:03.270] [OpenDAQMQTT] [info] Signal /RefDev1/Dev/RefDev0/IO/AI/RefCh1/Sig/AI1Time doesn't has domain signal assigned, skipping -[tid: 29784][2025-09-15 11:17:03.270] [OpenDAQMQTT] [info] Adding the Signal to reader: /RefDev1/Dev/RefDev0/IO/AI/ProtectedChannel/Sig/AI2; -[tid: 29784][2025-09-15 11:17:03.270] [OpenDAQMQTT] [info] Signal /RefDev1/Dev/RefDev0/IO/AI/ProtectedChannel/Sig/AI2Time doesn't has domain signal assigned, skipping -[tid: 29784][2025-09-15 11:17:03.270] [OpenDAQMQTT] [info] Signal /RefDev1/Dev/RefDev0/Sig/Time doesn't has domain signal assigned, skipping -[tid: 29812][2025-09-15 11:17:03.271] [OpenDAQMQTT] [info] Streaming-to-device read thread started -[tid: 29784][2025-09-15 11:17:03.271] [OpenDAQMQTT] [info] Added Component: /RefDev1/Srv/OpenDAQMQTT; -Press "enter" to exit the application... -[tid: 29811][2025-09-15 11:17:03.276] [OpenDAQMQTT] [info] MQTT: Connection established - -``` -In this case you can see messages on the *mosquitto_sub* side: - -```shell -user@machine:$ mosquitto_sub -h 127.0.0.1 -t "openDAQ/#" -v -openDAQ/RefDev1/$signals ["openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0","openDAQ/RefDev1/IO/AI/RefCh1/Sig/AI1","openDAQ/RefDev1/Dev/RefDev0/IO/AI/RefCh0/Sig/AI0","openDAQ/RefDev1/Dev/RefDev0/IO/AI/RefCh1/Sig/AI1","openDAQ/RefDev1/Dev/RefDev0/IO/AI/ProtectedChannel/Sig/AI2"] -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":1.243449435824274,"timestamp":1757928009227270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":0.9369065729286229,"timestamp":1757928009228270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":0.6266661678215204,"timestamp":1757928009229270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":0.3139525976465657,"timestamp":1757928009230270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":-1.6081226496766366e-15,"timestamp":1757928009231270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":-0.31395259764656677,"timestamp":1757928009232270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":-0.6266661678215214,"timestamp":1757928009233270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":-0.9369065729286239,"timestamp":1757928009234270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":-1.2434494358242752,"timestamp":1757928009235270} -openDAQ/RefDev1/IO/AI/RefCh0/Sig/AI0 {"value":-1.5450849718747386,"timestamp":1757928009236270} -<...> -``` +The *ref-dev-mqtt-sub* application is a console example which subscribes to an available MQTT openDAQ device and prints signal samples. \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 25c3842e..3636c2e5 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1,6 @@ cmake_minimum_required(VERSION 3.16) -add_subdirectory(ref-dev-mqtt-pub) +if (OPENDAQ_MQTT_ENABLE_EXAMPLE_APPS) + add_subdirectory(ref-dev-mqtt-pub) + add_subdirectory(ref-dev-mqtt-sub) +endif() diff --git a/examples/InputArgs.h b/examples/InputArgs.h new file mode 100644 index 00000000..8435d576 --- /dev/null +++ b/examples/InputArgs.h @@ -0,0 +1,103 @@ +#pragma once +#include +#include +#include +#include +#include + +class InputArgs +{ +public: + void addArg(const std::string& name, const std::string& description, bool hasValue = false) + { + argDescriptions[name] = {description, hasValue}; + } + + void parse(int argc, char* argv[]) + { + parsedArgs.clear(); + argValues.clear(); + positionalArgs.clear(); + + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + if (arg.rfind("--", 0) == 0) + { + auto eqPos = arg.find('='); + if (eqPos != std::string::npos) + { + std::string key = arg.substr(0, eqPos); + std::string value = arg.substr(eqPos + 1); + argValues[key] = value; + parsedArgs.push_back(key); + } + else if (i + 1 < argc && argDescriptions[arg].hasValue) + { + argValues[arg] = argv[i + 1]; + parsedArgs.push_back(arg); + ++i; + } + else + { + parsedArgs.push_back(arg); + } + } + else + { + positionalArgs.push_back(arg); + } + } + } + + bool hasArg(const std::string& name) const + { + return std::find(parsedArgs.begin(), parsedArgs.end(), name) != parsedArgs.end(); + } + + std::string getArgValue(const std::string& name, const std::string& defaultValue = "") const + { + auto it = argValues.find(name); + if (it != argValues.end()) + return it->second; + return defaultValue; + } + + const std::vector& getPositionalArgs() const + { + return positionalArgs; + } + + bool hasUnknownArgs() const + { + for (const auto& arg : parsedArgs) + { + if (argDescriptions.find(arg) == argDescriptions.end()) + return true; + } + return false; + } + + void printHelp() const + { + std::cout << "Available arguments:" << std::endl; + for (const auto& [name, descStruct] : argDescriptions) + { + std::cout << " " << name; + if (descStruct.hasValue) + std::cout << " "; + std::cout << " : " << descStruct.description << std::endl; + } + } + +private: + struct ArgDesc + { + std::string description; + bool hasValue; + }; + std::unordered_map argDescriptions; + std::vector parsedArgs; + std::unordered_map argValues; + std::vector positionalArgs; +}; \ No newline at end of file diff --git a/examples/ref-dev-mqtt-pub/src/CMakeLists.txt b/examples/ref-dev-mqtt-pub/src/CMakeLists.txt index 8ccc98a1..cd216504 100644 --- a/examples/ref-dev-mqtt-pub/src/CMakeLists.txt +++ b/examples/ref-dev-mqtt-pub/src/CMakeLists.txt @@ -4,4 +4,5 @@ add_compile_definitions(MODULE_PATH="${OPENDAQ_MODULES_DIR}") add_executable(${EXAMPLE_PROJECT_NAME} ref-dev-mqtt-pub.cpp) add_dependencies(${EXAMPLE_PROJECT_NAME} daq::ref_device_module) -target_link_libraries(${EXAMPLE_PROJECT_NAME} PRIVATE daq::opendaq) \ No newline at end of file +target_link_libraries(${EXAMPLE_PROJECT_NAME} PRIVATE daq::opendaq) +target_include_directories(${EXAMPLE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../) \ No newline at end of file diff --git a/examples/ref-dev-mqtt-pub/src/ref-dev-mqtt-pub.cpp b/examples/ref-dev-mqtt-pub/src/ref-dev-mqtt-pub.cpp index 3ee4ca13..dd6cf021 100644 --- a/examples/ref-dev-mqtt-pub/src/ref-dev-mqtt-pub.cpp +++ b/examples/ref-dev-mqtt-pub/src/ref-dev-mqtt-pub.cpp @@ -1,57 +1,15 @@ #include +#include "../../InputArgs.h" #include using namespace daq; -namespace -{ -class InputArgs -{ -public: - void addArg(const std::string& name, const std::string& description) - { - argDescriptions[name] = description; - } - - void parse(int argc, char* argv[]) - { - parsedArgs.clear(); - for (int i = 1; i < argc; ++i) - parsedArgs.push_back(argv[i]); - } - - bool hasArg(const std::string& name) const - { - return std::find(parsedArgs.begin(), parsedArgs.end(), name) != parsedArgs.end(); - } - - bool hasUnknownArgs() const - { - for (const auto& arg : parsedArgs) { - if (argDescriptions.find(arg) == argDescriptions.end()) - return true; - } - return false; - } - - void printHelp() const - { - std::cout << "Available arguments:" << std::endl; - for (const auto& [name, desc] : argDescriptions) - std::cout << " " << name << " : " << desc << std::endl; - } - -private: - std::unordered_map argDescriptions; - std::vector parsedArgs; -}; - -} // end of namespace int main(int argc, char* argv[]) { InputArgs args; args.addArg("--help", "Show help message"); + args.addArg("--address", "MQTT broker address", true); args.parse(argc, argv); if (args.hasArg("--help") || args.hasUnknownArgs()) { @@ -59,33 +17,29 @@ int main(int argc, char* argv[]) return 0; } - using namespace std::chrono_literals; - StringPtr loggerPath = "ref_device_simulator.log"; - - auto users = List(); - users.pushBack(User("opendaq", "$2b$10$bqZWNEd.g1R1Q1inChdAiuDr5lbal33bBNOehlCwuWcxRH5weF3hu")); // password: opendaq - users.pushBack(User("root", "$2b$10$k/Tj3yqFV7uQz42UCJK2n.4ECd.ySQ2Sfd81Kx.xfuMOeluvA/Vpy", {"admin"})); // password: root - const AuthenticationProviderPtr authenticationProvider = StaticAuthenticationProvider(true, users); - - PropertyObjectPtr config = PropertyObject(); - config.addProperty(StringProperty("Name", "Reference device simulator")); - config.addProperty(StringProperty("LocalId", "RefDevSimulator")); - config.addProperty(StringProperty("SerialNumber", "sim01")); - config.addProperty(BoolProperty("EnableLogging", true)); - config.addProperty(StringProperty("LoggingPath", loggerPath)); + std::string brokerAddress = args.getArgValue("--address", "127.0.0.1"); const InstancePtr instance = InstanceBuilder().addModulePath(MODULE_PATH) - .addDiscoveryServer("mdns") - .setRootDevice("daqref://device1", config) - .setLogger(Logger(loggerPath)) - .setAuthenticationProvider(authenticationProvider) + .setRootDevice("daqref://device0") .build(); - - auto refDevice = instance.addDevice("daqref://device0"); - refDevice.setPropertyValue("EnableProtectedChannel", true); + auto refDevice = instance.getRootDevice(); + refDevice.setPropertyValue("NumberOfChannels", 4); + + const auto channels = refDevice.getChannelsRecursive(); + channels[0].setPropertyValue("UseGlobalSampleRate", false); + channels[0].setPropertyValue("SampleRate", 10); + channels[0].setPropertyValue("Frequency", 1); + channels[0].setPropertyValue("Waveform", 1); + channels[1].setPropertyValue("UseGlobalSampleRate", false); + channels[1].setPropertyValue("SampleRate", 20); + channels[1].setPropertyValue("Frequency", 1); + channels[1].setPropertyValue("Waveform", 3); + channels[2].setPropertyValue("UseGlobalSampleRate", false); + channels[2].setPropertyValue("SampleRate", 200); + channels[2].setPropertyValue("Frequency", 4); auto serverConfig = instance.getAvailableServerTypes().get("OpenDAQMQTT").createDefaultConfig(); - serverConfig.setPropertyValue("BrokerAddress", "127.0.0.1"); + serverConfig.setPropertyValue("MqttBrokerAddress", brokerAddress); const auto mqttServer = instance.addServer("OpenDAQMQTT", serverConfig); std::cout << "Press \"enter\" to exit the application..." << std::endl; diff --git a/examples/ref-dev-mqtt-sub/CMakeLists.txt b/examples/ref-dev-mqtt-sub/CMakeLists.txt new file mode 100644 index 00000000..91eaee8c --- /dev/null +++ b/examples/ref-dev-mqtt-sub/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +set(EXAMPLE_PROJECT_NAME "ref-dev-mqtt-sub") + +project(${EXAMPLE_PROJECT_NAME} LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +add_subdirectory(src) diff --git a/examples/ref-dev-mqtt-sub/src/CMakeLists.txt b/examples/ref-dev-mqtt-sub/src/CMakeLists.txt new file mode 100644 index 00000000..c7621cc2 --- /dev/null +++ b/examples/ref-dev-mqtt-sub/src/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + +add_compile_definitions(MODULE_PATH="${OPENDAQ_MODULES_DIR}") + +add_executable(${EXAMPLE_PROJECT_NAME} ref-dev-mqtt-sub.cpp) +target_link_libraries(${EXAMPLE_PROJECT_NAME} PRIVATE daq::opendaq) +target_include_directories(${EXAMPLE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../) diff --git a/examples/ref-dev-mqtt-sub/src/ref-dev-mqtt-sub.cpp b/examples/ref-dev-mqtt-sub/src/ref-dev-mqtt-sub.cpp new file mode 100644 index 00000000..77a06654 --- /dev/null +++ b/examples/ref-dev-mqtt-sub/src/ref-dev-mqtt-sub.cpp @@ -0,0 +1,66 @@ +#include +#include "../../InputArgs.h" + +#include + +using namespace daq; + +int main(int argc, char* argv[]) +{ + InputArgs args; + args.addArg("--help", "Show help message"); + args.addArg("--address", "MQTT broker address", true); // If you want to support --address for sub + args.parse(argc, argv); + + if (args.hasArg("--help") || args.hasUnknownArgs()) { + args.printHelp(); + return 0; + } + + std::string brokerAddress = args.getArgValue("--address", "127.0.0.1"); + + const InstancePtr instance = InstanceBuilder().addModulePath(MODULE_PATH).build(); + auto brokerDevice = instance.addDevice("daq.mqtt://" + brokerAddress); + auto availableDeviceNodes = brokerDevice.getAvailableFunctionBlockTypes(); + + if (availableDeviceNodes.getCount() == 0) { + std::cout << "No function block available from the device." << std::endl; + return -1; + } + + std::vector fbList; + for (const auto& [key, value] : availableDeviceNodes) { + std::cout << "Available function block: " << key << std::endl; + fbList.push_back(brokerDevice.addFunctionBlock(key)); + } + std::cout << "Try to connect the first one (" << fbList[0].getLocalId() << ")" << std::endl; + + auto signals = fbList[0].getSignals(); + std::vector readers; + for (const auto& s : signals) { + readers.emplace_back(daq::StreamReader(s, ReadTimeoutType::Any)); + } + + std::thread t1([readers]() { + constexpr int size = 1000; + double samples[size]; + uint64_t timestamps[size]; + while (true) { + for (int iRdr = 0; iRdr < readers.size(); ++iRdr) { + const auto& reader = readers[iRdr]; + while (!reader.getEmpty()) { + daq::SizeT count = size; + reader.readWithDomain(samples, timestamps, &count); + for (daq::SizeT i = 0; i < count; ++i) + std::cout << "READER #" << iRdr << " - Sample: " << samples[i] << " Timestamp: " << timestamps[i] << std::endl; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + }); + t1.detach(); + + std::cout << "Press \"enter\" to exit the application..." << std::endl; + std::cin.get(); + return 0; +} diff --git a/external/mqtt/CMakeLists.txt b/external/mqtt/CMakeLists.txt index cea3bc6b..96dca57d 100644 --- a/external/mqtt/CMakeLists.txt +++ b/external/mqtt/CMakeLists.txt @@ -16,6 +16,7 @@ set(PAHO_WITH_SSL ON CACHE BOOL "Enable SSL support" FORCE) set(PAHO_BUILD_STATIC ON CACHE BOOL "Build static paho library" FORCE) set(PAHO_BUILD_SHARED OFF CACHE BOOL "Build dynamic paho library" FORCE) set(CMAKE_POSITION_INDEPENDENT_CODE ${PAHO_BUILD_STATIC} CACHE BOOL "" FORCE) +set(PAHO_ENABLE_TESTING ON CACHE BOOL "" FORCE) # Now add the project add_subdirectory(${paho_mqtt_c_SOURCE_DIR} ${paho_mqtt_c_BINARY_DIR}) diff --git a/external/opendaq/CMakeLists.txt b/external/opendaq/CMakeLists.txt index a886ad1f..12768c24 100644 --- a/external/opendaq/CMakeLists.txt +++ b/external/opendaq/CMakeLists.txt @@ -2,11 +2,37 @@ set(OPENDAQ_ENABLE_TESTS false) FetchContent_Declare( opendaq - GIT_REPOSITORY git@github.com:openDAQ/openDAQ.git - GIT_TAG b38e835b591317fdd1f9f84aad2bd79f04c06fd3 + GIT_REPOSITORY https://github.com/openDAQ/openDAQ.git + GIT_TAG v3.29.0-rc GIT_PROGRESS ON EXCLUDE_FROM_ALL SYSTEM ) FetchContent_MakeAvailable(opendaq) + +# Ensure we have the source dir variable (set by FetchContent) +if(NOT DEFINED opendaq_SOURCE_DIR) + FetchContent_GetProperties(opendaq) + if(NOT opendaq_POPULATED) + FetchContent_Populate(opendaq) + endif() +endif() + +set(OPENDAQ_VERSION_FILE "${opendaq_SOURCE_DIR}/opendaq_version") + +if(EXISTS "${OPENDAQ_VERSION_FILE}") + file(READ "${OPENDAQ_VERSION_FILE}" _raw_version) + string(STRIP "${_raw_version}" _raw_version) + + string(REGEX REPLACE "^([0-9]+\\.[0-9]+\\.[0-9]+).*" "\\1" OPENDAQ_PACKAGE_VERSION "${_raw_version}") + + message(STATUS "openDAQ version: ${OPENDAQ_PACKAGE_VERSION}") + + set(OPENDAQ_PACKAGE_VERSION "${OPENDAQ_PACKAGE_VERSION}" CACHE STRING "openDAQ package version" FORCE) +else() + message(WARNING "opendaq_version file not found at: ${OPENDAQ_VERSION_FILE}") +endif() + + + diff --git a/mqtt_streaming_client_module/CMakeLists.txt b/mqtt_streaming_client_module/CMakeLists.txt new file mode 100644 index 00000000..91f88d29 --- /dev/null +++ b/mqtt_streaming_client_module/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) + +set(MQTT_CLIENT_MODULE_VERSION ${OPENDAQ_PACKAGE_VERSION}) +set(MQTT_CLIENT_MODULE_PRJ_NAME "OpenDaqMqttClientModule") + +message(STATUS "${MQTT_CLIENT_MODULE_PRJ_NAME} version: ${MQTT_CLIENT_MODULE_VERSION}") +project(${MQTT_CLIENT_MODULE_PRJ_NAME} VERSION ${MQTT_CLIENT_MODULE_VERSION} LANGUAGES C CXX) + +add_subdirectory(src) +if (OPENDAQ_MQTT_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/mqtt_streaming_client_module/include/mqtt_streaming_client_module/common.h b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/common.h new file mode 100644 index 00000000..c8e8ff84 --- /dev/null +++ b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/common.h @@ -0,0 +1,21 @@ +/* + * Copyright 2022-2025 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_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(mqtt_streaming_client_module) +#define END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/mqtt_streaming_client_module/include/mqtt_streaming_client_module/constants.h b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/constants.h new file mode 100644 index 00000000..a6efa8af --- /dev/null +++ b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/constants.h @@ -0,0 +1,36 @@ +#pragma once + +#include "common.h" + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE + +static const char* DaqMqttDeviceTypeId = "OpenDAQMQTTStreaming"; +static const char* DaqMqttProtocolId = "OpenDAQMQTTStreaming"; +static const char* DaqMqttDevicePrefix = "daq.mqtt"; +static const char* MqttScheme = "mqtt"; + +static const char* MODULE_NAME = "OpenDAQMQTTClientModule"; +static const char* MODULE_ID = "OpenDAQMQTTClientModule"; +static const char* SHORT_MODULE_NAME = "MQTTClient"; +static const char* PROTOCOL_NAME = "OpenDAQMQTT"; +static const char* CONNECTION_TYPE = "TCP/IP"; + +static constexpr const char* DEFAULT_BROKER_ADDRESS = "127.0.0.1"; +static constexpr uint16_t DEFAULT_PORT = 1883; +static constexpr const char* DEFAULT_USERNAME = ""; +static constexpr const char* DEFAULT_PASSWORD = ""; +static constexpr uint32_t DEFAULT_INIT_DELAY = 3000; // ms + +static constexpr const char* PROPERTY_NAME_MQTT_BROKER_ADDRESS = "MqttBrokerAddress"; +static constexpr const char* PROPERTY_NAME_MQTT_BROKER_PORT = "MqttBrokerPort"; +static constexpr const char* PROPERTY_NAME_MQTT_USERNAME = "MqttUsername"; +static constexpr const char* PROPERTY_NAME_MQTT_PASSWORD = "MqttPassword"; +static constexpr const char* PROPERTY_NAME_INIT_DELAY = "InitDelay"; +static constexpr const char* PROPERTY_NAME_SIGNAL_LIST = "SignalList"; + +static const char* TOPIC_ALL_SIGNALS = "openDAQ/+/$signals"; + +static const char* MQTT_LOCAL_DEVICE_ID_PREFIX = "MqttDevice"; +static const char* MQTT_DEVICE_NAME = "MqttStreamingClientPseudoDevice"; + +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE diff --git a/mqtt_streaming_client_module/include/mqtt_streaming_client_module/module_dll.h b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/module_dll.h new file mode 100644 index 00000000..b0f19906 --- /dev/null +++ b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/module_dll.h @@ -0,0 +1,20 @@ +/* + * Copyright 2022-2025 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(MqttStreamingClientModule) diff --git a/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_receiver_fb_impl.h b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_receiver_fb_impl.h new file mode 100644 index 00000000..94b4c492 --- /dev/null +++ b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_receiver_fb_impl.h @@ -0,0 +1,63 @@ +/* + * Copyright 2022-2025 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 +#include + +#include "MqttAsyncClient.h" + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE + +class MqttReceiverFbImpl final : public FunctionBlock +{ +public: + explicit MqttReceiverFbImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const FunctionBlockTypePtr& type, + const StringPtr& localId, + std::shared_ptr subscriber, + const PropertyObjectPtr& config = nullptr); + ~MqttReceiverFbImpl() override; + +private: + std::unordered_map outputSignals; + std::unordered_map outputDomainSignals; + + std::shared_ptr subscriber; + DictObjectPtr subscribedSignals; + + std::mutex sync; + + void createSignals(); + + void parseMessage(mqtt::MqttMessage& msg); + void createDataPacket(const std::string& topic, double value, UInt timestamp); + + void initProperties(const PropertyObjectPtr& config); + void readProperties(); + + void onSignalsMessage(const mqtt::MqttAsyncClient& subscriber, mqtt::MqttMessage& msg); + + std::string buildSignalNameFromTopic(std::string topic) const; + std::string buildDomainSignalNameFromTopic(std::string topic) const; + +}; + +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE diff --git a/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_streaming_client_module_impl.h b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_streaming_client_module_impl.h new file mode 100644 index 00000000..afa6453a --- /dev/null +++ b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_streaming_client_module_impl.h @@ -0,0 +1,47 @@ +/* + * Copyright 2022-2025 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 + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE + +class MqttStreamingClientModule final : public Module +{ +public: + MqttStreamingClientModule(ContextPtr context); + + ListPtr onGetAvailableDevices() override; + DictPtr onGetAvailableDeviceTypes() override; + DevicePtr onCreateDevice(const StringPtr& connectionString, + const ComponentPtr& parent, + const PropertyObjectPtr& config) override; + bool acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); + Bool onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) override; // ??? + +private: + void extractConnectionParams(const StringPtr& connectionString, const PropertyObjectPtr& config, std::string& hostType); + static DeviceTypePtr createDeviceType(); + static PropertyObjectPtr createDefaultConfig(); + static PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& config); + + std::mutex sync; + DevicePtr device; +}; + +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE diff --git a/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_streaming_device_impl.h b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_streaming_device_impl.h new file mode 100644 index 00000000..05c77b14 --- /dev/null +++ b/mqtt_streaming_client_module/include/mqtt_streaming_client_module/mqtt_streaming_device_impl.h @@ -0,0 +1,69 @@ +/* + * Copyright 2022-2025 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 "MqttAsyncClient.h" +#include "MqttSettings.h" +#include +#include +#include +#include +#include "MqttDataWrapper.h" + + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE + +class MqttStreamingDeviceImpl : public Device +{ +public: + explicit MqttStreamingDeviceImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const PropertyObjectPtr& config); + +protected: + static std::atomic localIndex; + static std::string getLocalId(); + + void removed() override; + DeviceInfoPtr onGetInfo() override; + + bool allowAddFunctionBlocksFromModules() override + { + return true; + }; + DictPtr onGetAvailableFunctionBlockTypes() override; + FunctionBlockPtr onAddFunctionBlock(const StringPtr& typeId, const PropertyObjectPtr& config) override; + + void initMqttSubscriber(); + bool waitForConnection(const int timeoutMs); + void receiveSignalTopics(const int timeoutMs); + void onSignalsMessage(const mqtt::MqttAsyncClient& subscriber, mqtt::MqttMessage& msg); + + DictObjectPtr fbTypes; + + StringPtr connectionString; + EnumerationPtr connectionStatus; + + std::shared_ptr subscriber; + Mqtt::Utils::Settings::MqttConnectionSettings connectionSettings; + + std::promise connectedPromise; + std::future connectedFuture; + std::atomic connectedDone{false}; + std::unordered_map> deviceMap; +}; + +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE diff --git a/mqtt_streaming_client_module/src/CMakeLists.txt b/mqtt_streaming_client_module/src/CMakeLists.txt new file mode 100644 index 00000000..ac250ae6 --- /dev/null +++ b/mqtt_streaming_client_module/src/CMakeLists.txt @@ -0,0 +1,62 @@ +set(LIB_NAME mqtt_stream_cli_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + module_dll.h + mqtt_streaming_client_module_impl.h + mqtt_streaming_device_impl.h + mqtt_receiver_fb_impl.h + constants.h +) + +set(SRC_Srcs module_dll.cpp + mqtt_streaming_client_module_impl.cpp + mqtt_streaming_device_impl.cpp + mqtt_receiver_fb_impl.cpp +) + +prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("common" FILES ${MODULE_HEADERS_DIR}/common.h + ${MODULE_HEADERS_DIR}/constants.h +) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/mqtt_streaming_client_module_impl.h + ${MODULE_HEADERS_DIR}/module_dll.h + module_dll.cpp + mqtt_streaming_client_module_impl.cpp +) + +source_group("device" FILES ${MODULE_HEADERS_DIR}/mqtt_streaming_device_impl.h + mqtt_streaming_device_impl.cpp +) + +source_group("functionalBlock" FILES ${MODULE_HEADERS_DIR}/mqtt_receiver_fb_impl.h + dispatch.h + mqtt_receiver_fb_impl.cpp +) + +find_package(Boost REQUIRED COMPONENTS algorithm) + +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 + PRIVATE mqtt_streaming_protocol + $ +) + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + $ +) + +opendaq_set_module_properties(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) +create_version_header(${LIB_NAME}) + diff --git a/mqtt_streaming_client_module/src/module_dll.cpp b/mqtt_streaming_client_module/src/module_dll.cpp new file mode 100644 index 00000000..b8015666 --- /dev/null +++ b/mqtt_streaming_client_module/src/module_dll.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +using namespace daq::modules::mqtt_streaming_client_module; + +DEFINE_MODULE_EXPORTS(MqttStreamingClientModule) + diff --git a/mqtt_streaming_client_module/src/mqtt_receiver_fb_impl.cpp b/mqtt_streaming_client_module/src/mqtt_receiver_fb_impl.cpp new file mode 100644 index 00000000..aeaa2946 --- /dev/null +++ b/mqtt_streaming_client_module/src/mqtt_receiver_fb_impl.cpp @@ -0,0 +1,182 @@ +#include "MqttDataWrapper.h" +#include "mqtt_streaming_client_module/constants.h" +#include "opendaq/data_packet_ptr.h" +#include "opendaq/packet_factory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE + +MqttReceiverFbImpl::MqttReceiverFbImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const FunctionBlockTypePtr& type, + const StringPtr& localId, + std::shared_ptr subscriber, + const PropertyObjectPtr& config) + : FunctionBlock(type, ctx, parent, localId), + subscriber(subscriber) +{ + initComponentStatus(); + + if (config.assigned()) + initProperties(config); + else + initProperties(type.createDefaultConfig()); + createSignals(); + + for (const auto& topic : subscribedSignals.getKeys()) + { + subscriber + ->setMessageArrivedCb(topic, + std::bind(&MqttReceiverFbImpl::onSignalsMessage, this, std::placeholders::_1, std::placeholders::_2)); + auto ok = subscriber->subscribe(topic, 1); + if (!ok) + LOG_W("Failed to subscribe to the topic: {}", topic); + } + setComponentStatus(ComponentStatus::Ok); +} + +MqttReceiverFbImpl::~MqttReceiverFbImpl() +{ + for (const auto& topic : subscribedSignals.getKeys()) + { + subscriber->setMessageArrivedCb(topic, nullptr); + subscriber->unsubscribe(topic); + } +} +void MqttReceiverFbImpl::onSignalsMessage(const mqtt::MqttAsyncClient& subscriber, mqtt::MqttMessage& msg) +{ + parseMessage(msg); +} + +void MqttReceiverFbImpl::initProperties(const PropertyObjectPtr& config) +{ + for (const auto& prop : config.getAllProperties()) + { + const auto propName = prop.getName(); + if (!objPtr.hasProperty(propName)) + { + if (const auto internalProp = prop.asPtrOrNull(true); internalProp.assigned()) + { + objPtr.addProperty(internalProp.clone()); + } + } + objPtr.setPropertyValue(propName, prop.getValue()); + } + readProperties(); +} + +void MqttReceiverFbImpl::readProperties() +{ + auto lock = std::lock_guard(sync); + auto prop = objPtr.getPropertyValue(PROPERTY_NAME_SIGNAL_LIST).asPtr(); + subscribedSignals = Dict(); + for (const auto& [topic, descriptor] : prop) + { + auto signalId = topic.asPtr(); + if (signalId.assigned()) + { + LOG_I("Signal in list: {}", signalId.toStdString()); + subscribedSignals.set(signalId, descriptor); + } + } +} + +void MqttReceiverFbImpl::createDataPacket(const std::string& topic, double value, UInt timestamp) +{ + auto lock = std::lock_guard(sync); + auto signalIter = outputSignals.find(topic); + auto dSignalIter = outputDomainSignals.find(topic); + if (signalIter == outputSignals.end() || dSignalIter == outputDomainSignals.end()) + { + return; + } + auto signal = signalIter->second; + auto dSignal = dSignalIter->second; + + DataPacketPtr outputDomainPacket = DataPacket(signal.getDomainSignal().getDescriptor(), 1); + std::memcpy(outputDomainPacket.getRawData(), ×tamp, sizeof(timestamp)); + DataPacketPtr outputPacket = DataPacketWithDomain(outputDomainPacket, signal.getDescriptor(), 1); + + auto outputData = reinterpret_cast(outputPacket.getRawData()); + *outputData = value; + signal.sendPacket(outputPacket); + dSignal.sendPacket(outputDomainPacket); +} + +void MqttReceiverFbImpl::parseMessage(mqtt::MqttMessage& msg) +{ + std::string topic(msg.getTopic()); + std::string jsonObjStr(msg.getData().begin(), msg.getData().end()); + auto [status, data] = mqtt::MqttDataWrapper::parseSampleData(jsonObjStr); + if (status.ok) + { + createDataPacket(topic, data.value, data.timestamp); + } + else + { + for (const auto& s : status.msg) + { + LOG_W("Data parsing: {}", s); + } + } +} + +void MqttReceiverFbImpl::createSignals() +{ + auto lock = std::lock_guard(sync); + for (const auto& [topic, descriptor] : subscribedSignals) + { + LOG_I("Subscribing to topic: {}", topic); + std::string signalName = topic; + boost::replace_all(signalName, "/", "_"); + + auto signalDsc = descriptor; + auto getEpoch = []() -> std::string + { + const std::time_t epochTime = std::chrono::system_clock::to_time_t(std::chrono::time_point{}); + char buf[48]; + strftime(buf, sizeof buf, "%Y-%m-%dT%H:%M:%SZ", gmtime(&epochTime)); + return {buf}; + }; + + const auto domainSignalDsc = DataDescriptorBuilder() + .setSampleType(SampleType::UInt64) + .setUnit(Unit("s", -1, "seconds", "time")) + .setTickResolution(Ratio(1, 1'000'000)) + .setOrigin(getEpoch()) + .setName("Time") + .build(); + + auto refS = outputSignals.emplace(std::make_pair(topic, createAndAddSignal(buildSignalNameFromTopic(topic), signalDsc))).first; + auto refSD = outputDomainSignals + .emplace(std::make_pair(topic, createAndAddSignal(buildDomainSignalNameFromTopic(topic), domainSignalDsc, false))) + .first; + refS->second->setDomainSignal(refSD->second); + } +} + +std::string MqttReceiverFbImpl::buildSignalNameFromTopic(std::string topic) const +{ + boost::replace_all(topic, "/", "_"); + topic += "_Mqtt"; + return topic; +} + +std::string MqttReceiverFbImpl::buildDomainSignalNameFromTopic(std::string topic) const +{ + boost::replace_all(topic, "/", "_"); + topic += std::string("_Mqtt") + "_domain"; + return topic; +} + +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE diff --git a/mqtt_streaming_client_module/src/mqtt_streaming_client_module_impl.cpp b/mqtt_streaming_client_module/src/mqtt_streaming_client_module_impl.cpp new file mode 100644 index 00000000..a877ed4c --- /dev/null +++ b/mqtt_streaming_client_module/src/mqtt_streaming_client_module_impl.cpp @@ -0,0 +1,254 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE + +static const std::regex RegexIpv6Hostname(R"(^(.+://)?(\[[a-fA-F0-9:]+(?:\%[a-zA-Z0-9_\.-~]+)?\])(?::(\d+))?(/.*)?$)"); +static const std::regex RegexIpv4Hostname(R"(^(.+://)?([^:/\s]+)(?::(\d+))?(/.*)?$)"); + +MqttStreamingClientModule::MqttStreamingClientModule(ContextPtr context) + : Module(MODULE_NAME, + daq::VersionInfo(MQTT_STREAM_CLI_MODULE_MAJOR_VERSION, + MQTT_STREAM_CLI_MODULE_MINOR_VERSION, + MQTT_STREAM_CLI_MODULE_PATCH_VERSION), + std::move(context), + MODULE_ID) +{ + loggerComponent = this->context.getLogger().getOrAddComponent(SHORT_MODULE_NAME); +} + +ListPtr MqttStreamingClientModule::onGetAvailableDevices() +{ + auto availableDevices = List(); + return availableDevices; +} + +DictPtr MqttStreamingClientModule::onGetAvailableDeviceTypes() +{ + auto result = Dict(); + + auto deviceType = createDeviceType(); + result.set(deviceType.getId(), deviceType); + return result; +} + +DevicePtr +MqttStreamingClientModule::onCreateDevice(const StringPtr& connectionString, const ComponentPtr& parent, const PropertyObjectPtr& config) +{ + if (!connectionString.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException); + + PropertyObjectPtr configPtr = config; + if (!configPtr.assigned()) + configPtr = createDefaultConfig(); + else + configPtr = populateDefaultConfig(configPtr); + + if (!acceptsConnectionParameters(connectionString, configPtr)) + DAQ_THROW_EXCEPTION(InvalidParameterException); + + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Context is not available."); + + std::string hostType; + extractConnectionParams(connectionString, configPtr, hostType); + + std::string host = configPtr.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_ADDRESS); + Int port = configPtr.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_PORT); + + std::scoped_lock lock(sync); + + device = createWithImplementation(context, parent, configPtr); + + // Set the connection info for the device + ServerCapabilityConfigPtr connectionInfo = device.getInfo().getConfigurationConnectionInfo(); + + const auto addressInfo = AddressInfoBuilder() + .setAddress(host) + .setReachabilityStatus(AddressReachabilityStatus::Reachable) + .setType(hostType) + .setConnectionString(connectionString) + .build(); + + connectionInfo.setProtocolId(DaqMqttDeviceTypeId) + .setProtocolName(PROTOCOL_NAME) + .setProtocolType(ProtocolType::Streaming) + .setConnectionType(CONNECTION_TYPE) + .addAddress(host) + .setPort(port) + .setPrefix(DaqMqttDevicePrefix) + .setConnectionString(connectionString) + .addAddressInfo(addressInfo) + .freeze(); + + return device; +} + +PropertyObjectPtr MqttStreamingClientModule::populateDefaultConfig(const PropertyObjectPtr& config) +{ + const auto defConfig = createDefaultConfig(); + for (const auto& prop : defConfig.getAllProperties()) + { + const auto name = prop.getName(); + if (config.hasProperty(name)) + defConfig.setPropertyValue(name, config.getPropertyValue(name)); + } + + return defConfig; +} + +void MqttStreamingClientModule::extractConnectionParams(const StringPtr& connectionString, + const PropertyObjectPtr& config, + std::string& hostType) +{ + std::string urlString = connectionString.toStdString(); + std::smatch match; + + std::string prefix = ""; + std::string path = "/"; + std::string host; + int port = 0; + + bool parsed = false; + parsed = std::regex_search(urlString, match, RegexIpv6Hostname); + hostType = "IPv6"; + + if (!parsed) + { + parsed = std::regex_search(urlString, match, RegexIpv4Hostname); + hostType = "IPv4"; + } + + if (parsed) + { + prefix = match[1]; + host = match[2]; + + if (match[3].matched) + port = std::stoi(match[3]); + + if (match[4].matched) + path = match[4]; + } + else + DAQ_THROW_EXCEPTION(InvalidParameterException, "Host name not found in url: {}", connectionString); + + if (prefix != std::string(DaqMqttDevicePrefix) + "://") + DAQ_THROW_EXCEPTION(InvalidParameterException, "MQTT does not support connection string with prefix {}", prefix); + + if (!config.assigned()) + { + DAQ_THROW_EXCEPTION(InvalidParameterException, "Config is missing"); + } + if (port != 0 && config.hasProperty(PROPERTY_NAME_MQTT_BROKER_PORT)) + config.setPropertyValue(PROPERTY_NAME_MQTT_BROKER_PORT, Int(port)); + + if (config.hasProperty(PROPERTY_NAME_MQTT_BROKER_ADDRESS)) + config.setPropertyValue(PROPERTY_NAME_MQTT_BROKER_ADDRESS, host); +} + +bool MqttStreamingClientModule::acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config) +{ + std::string connStr = connectionString; + auto found = connStr.find(std::string(DaqMqttDevicePrefix) + "://"); + return found == 0; +} + +Bool MqttStreamingClientModule::onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) +{ + if (target.getProtocolId() != DaqMqttProtocolId) + return false; + + if (source.getConnectionType() != CONNECTION_TYPE) // ??? + return false; + + if (!source.getAddresses().assigned() || !source.getAddresses().getCount()) + { + LOG_W("Source server capability address is not available when filling in missing MQTT capability information.") + return false; + } + + const auto addrInfos = source.getAddressInfo(); + if (!addrInfos.assigned() || !addrInfos.getCount()) + { + LOG_W("Source server capability addressInfo is not available when filling in missing MQTT capability " + "information.") + return false; + } + + auto port = target.getPort(); + if (port == -1) + { + port = DEFAULT_PORT; + target.setPort(port); + LOG_W("MQTT server capability is missing port. Defaulting to {}.", port) + } + + const auto path = target.hasProperty("Path") ? target.getPropertyValue("Path") : ""; + const auto targetAddress = target.getAddresses(); + for (const auto& addrInfo : addrInfos) + { + const auto address = addrInfo.getAddress(); + if (auto it = std::find(targetAddress.begin(), targetAddress.end(), address); it != targetAddress.end()) + continue; + + const auto prefix = target.getPrefix(); + StringPtr connectionString; + if (source.getPrefix() == prefix) + connectionString = addrInfo.getConnectionString(); + else + connectionString = fmt::format("{}://{}:{}{}", prefix, address, port, path); + + const auto targetAddrInfo = AddressInfoBuilder() + .setAddress(address) + .setReachabilityStatus(addrInfo.getReachabilityStatus()) + .setType(addrInfo.getType()) + .setConnectionString(connectionString) + .build(); + + target.addAddressInfo(targetAddrInfo).setConnectionString(connectionString).addAddress(address); + } + + return true; +} + +DeviceTypePtr MqttStreamingClientModule::createDeviceType() +{ + return DeviceTypeBuilder() + .setId(DaqMqttDeviceTypeId) + .setName("MQTT enabled device") + .setDescription("Network device connected over MQTT protocol") + .setConnectionStringPrefix(DaqMqttDevicePrefix) + .setDefaultConfig(createDefaultConfig()) + .build(); +} + +PropertyObjectPtr MqttStreamingClientModule::createDefaultConfig() +{ + auto config = PropertyObject(); + + config.addProperty(StringProperty(PROPERTY_NAME_MQTT_BROKER_ADDRESS, DEFAULT_BROKER_ADDRESS)); + config.addProperty(StringProperty(PROPERTY_NAME_MQTT_USERNAME, DEFAULT_USERNAME)); + config.addProperty(StringProperty(PROPERTY_NAME_MQTT_PASSWORD, DEFAULT_PASSWORD)); + config.addProperty(IntProperty(PROPERTY_NAME_MQTT_BROKER_PORT, DEFAULT_PORT)); + config.addProperty(IntProperty(PROPERTY_NAME_INIT_DELAY, DEFAULT_INIT_DELAY)); + + return config; +} + +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE diff --git a/mqtt_streaming_client_module/src/mqtt_streaming_device_impl.cpp b/mqtt_streaming_client_module/src/mqtt_streaming_device_impl.cpp new file mode 100644 index 00000000..5ab1e7ef --- /dev/null +++ b/mqtt_streaming_client_module/src/mqtt_streaming_device_impl.cpp @@ -0,0 +1,191 @@ +#include "mqtt_streaming_client_module/constants.h" +#include "mqtt_streaming_client_module/mqtt_receiver_fb_impl.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE + +std::atomic MqttStreamingDeviceImpl::localIndex = 0; + +constexpr int MQTT_CLIENT_SYNC_DISCONNECT_TOUT = 3000; + +MqttStreamingDeviceImpl::MqttStreamingDeviceImpl(const ContextPtr& ctx, const ComponentPtr& parent, const PropertyObjectPtr& config) + : Device(ctx, parent, getLocalId()), + connectionStatus(Enumeration("ConnectionStatusType", "Connected", this->context.getTypeManager())), + subscriber(std::make_shared()) +{ + this->name = MQTT_DEVICE_NAME; + + connectionSettings.mqttUrl = config.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_ADDRESS).asPtr().toStdString(); + connectionSettings.port = config.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_PORT); + connectionSettings.username = config.getPropertyValue(PROPERTY_NAME_MQTT_USERNAME).asPtr().toStdString(); + connectionSettings.password = config.getPropertyValue(PROPERTY_NAME_MQTT_PASSWORD).asPtr().toStdString(); + connectionSettings.clientId = globalId.toStdString(); + + connectionString = + std::string(DaqMqttDevicePrefix) + "://" + connectionSettings.mqttUrl + ":" + std::to_string(connectionSettings.port); + + int initTimeout = config.getPropertyValue(PROPERTY_NAME_INIT_DELAY); + + initComponentStatus(); + + initMqttSubscriber(); + if (!waitForConnection(initTimeout)) + { + LOG_E("MQTT: could not connect to MQTT broker within {} ms", initTimeout); + DAQ_THROW_EXCEPTION(CreateFailedException, "could not connect to MQTT broker within {} ms", initTimeout); + } + + LOG_I("MQTT: Connection established"); + receiveSignalTopics(initTimeout); +} + +void MqttStreamingDeviceImpl::removed() +{ + bool disRes = subscriber->syncDisconnect(MQTT_CLIENT_SYNC_DISCONNECT_TOUT); + if (!disRes) + LOG_E("MQTT: disconnection was unsuccessful"); + Device::removed(); +} + +DeviceInfoPtr MqttStreamingDeviceImpl::onGetInfo() +{ + return DeviceInfo(connectionString, MQTT_DEVICE_NAME); +} + +void MqttStreamingDeviceImpl::initMqttSubscriber() +{ + subscriber->setServerURL(connectionSettings.mqttUrl); + subscriber->setClientId(connectionSettings.clientId); + subscriber->setUsernamePasswrod(connectionSettings.username, connectionSettings.password); + + connectedDone = false; + connectedPromise = std::promise(); + connectedFuture = connectedPromise.get_future(); + + subscriber->setConnectedCb( + [this] + { + bool expected = false; + if (connectedDone.compare_exchange_strong(expected, true)) + { + connectedPromise.set_value(true); + } + }); + + LOG_I("MQTT: Trying to connect to MQTT broker ({})", connectionSettings.mqttUrl); + subscriber->connect(); +} + +bool MqttStreamingDeviceImpl::waitForConnection(const int timeoutMs) +{ + bool res = + (connectedFuture.wait_for(std::chrono::milliseconds(timeoutMs)) == std::future_status::ready && connectedFuture.get() == true); + subscriber->setConnectedCb(nullptr); + return res; +} + +void MqttStreamingDeviceImpl::receiveSignalTopics(const int timeoutMs) +{ + subscriber->setMessageArrivedCb( + std::bind(&MqttStreamingDeviceImpl::onSignalsMessage, this, std::placeholders::_1, std::placeholders::_2)); + subscriber->subscribe(TOPIC_ALL_SIGNALS, 1); + + std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs)); // TODO : remove it! + + subscriber->unsubscribe(TOPIC_ALL_SIGNALS); + subscriber->setMessageArrivedCb(nullptr); +} + +void MqttStreamingDeviceImpl::onSignalsMessage(const mqtt::MqttAsyncClient& subscriber, mqtt::MqttMessage& msg) +{ + const std::string signalList(msg.getData().begin(), msg.getData().end()); + const std::string topic = msg.getTopic(); + auto [status, data] = mqtt::MqttDataWrapper::parseSignalDescriptors(topic, signalList); + if (status.ok) + { + deviceMap.insert({std::move(data.first), std::move(data.second)}); + } + else + { + for (const auto& s : status.msg) + LOG_W("Data error: {}", s); + } +} + +DictPtr MqttStreamingDeviceImpl::onGetAvailableFunctionBlockTypes() +{ + fbTypes = Dict(); + for (const auto& device : deviceMap) + { + auto defaultConfig = PropertyObject(); + auto signalDict = Dict(); + for (const auto& signal : device.second) + { + auto builder = DataDescriptorBuilder().setSampleType(SampleType::Float64); + if (!signal.name.empty()) + builder.setName(signal.name); + if (!signal.unit.empty()) + { + std::string symbol{""}; + std::string name{""}; + std::string quantity{""}; + if (signal.unit.size() > 0) + symbol = signal.unit[0]; + if (signal.unit.size() > 1) + name = signal.unit[1]; + if (signal.unit.size() > 2) + quantity = signal.unit[2]; + builder.setUnit(Unit(symbol, -1, name, quantity)); + } + auto signalDsc = builder.build(); + signalDict.set(signal.topic, signalDsc); + } + defaultConfig.addProperty(DictProperty(PROPERTY_NAME_SIGNAL_LIST, signalDict)); + + const auto fbType = FunctionBlockType(device.first, device.first, "", defaultConfig); + + fbTypes.set(fbType.getId(), fbType); + } + return fbTypes; +} + +FunctionBlockPtr MqttStreamingDeviceImpl::onAddFunctionBlock(const StringPtr& typeId, const PropertyObjectPtr& config) +{ + FunctionBlockPtr nestedFunctionBlock; + { + if (fbTypes.hasKey(typeId)) + { + auto fbTypePtr = fbTypes.getOrDefault(typeId); + nestedFunctionBlock = createWithImplementation(context, + functionBlocks, + fbTypePtr, + typeId, + subscriber, + config); + addNestedFunctionBlock(nestedFunctionBlock); + setComponentStatus(ComponentStatus::Ok); + } + else + { + setComponentStatusWithMessage(ComponentStatus::Error, "Function block type is not available: " + typeId.toStdString()); + } + } + return nestedFunctionBlock; +} + +std::string MqttStreamingDeviceImpl::getLocalId() +{ + return std::string(MQTT_LOCAL_DEVICE_ID_PREFIX + std::to_string(localIndex++)); +} +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_CLIENT_MODULE diff --git a/mqtt_streaming_client_module/tests/CMakeLists.txt b/mqtt_streaming_client_module/tests/CMakeLists.txt new file mode 100644 index 00000000..20e1f6f4 --- /dev/null +++ b/mqtt_streaming_client_module/tests/CMakeLists.txt @@ -0,0 +1,18 @@ +set(MODULE_NAME mqtt_stream_cli_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_mqtt_streaming_client_module.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + ${MODULE_NAME} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) diff --git a/mqtt_streaming_client_module/tests/test_app.cpp b/mqtt_streaming_client_module/tests/test_app.cpp new file mode 100644 index 00000000..d92f41f2 --- /dev/null +++ b/mqtt_streaming_client_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/mqtt_streaming_client_module/tests/test_mqtt_streaming_client_module.cpp b/mqtt_streaming_client_module/tests/test_mqtt_streaming_client_module.cpp new file mode 100644 index 00000000..5a67f568 --- /dev/null +++ b/mqtt_streaming_client_module/tests/test_mqtt_streaming_client_module.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +using MqttStreamingClientModuleTest = testing::Test; +using namespace daq; +using namespace daq::modules::mqtt_streaming_client_module; + +static ModulePtr CreateModule() +{ + ModulePtr module; + createModule(&module, NullContext()); + return module; +} + +TEST_F(MqttStreamingClientModuleTest, CreateModule) +{ + IModule* module = nullptr; + ErrCode errCode = createModule(&module, NullContext()); + ASSERT_TRUE(OPENDAQ_SUCCEEDED(errCode)); + + ASSERT_NE(module, nullptr); + module->releaseRef(); +} + +TEST_F(MqttStreamingClientModuleTest, ModuleName) +{ + auto module = CreateModule(); + ASSERT_EQ(module.getModuleInfo().getName(), daq::modules::mqtt_streaming_client_module::MODULE_NAME); +} + +TEST_F(MqttStreamingClientModuleTest, VersionAvailable) +{ + auto module = CreateModule(); + ASSERT_TRUE(module.getModuleInfo().getVersionInfo().assigned()); +} + +TEST_F(MqttStreamingClientModuleTest, VersionCorrect) +{ + auto module = CreateModule(); + auto version = module.getModuleInfo().getVersionInfo(); + + ASSERT_EQ(version.getMajor(), MQTT_STREAM_CLI_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), MQTT_STREAM_CLI_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), MQTT_STREAM_CLI_MODULE_PATCH_VERSION); +} + +TEST_F(MqttStreamingClientModuleTest, EnumerateDevices) +{ + auto module = CreateModule(); + + ListPtr deviceInfo; + ASSERT_NO_THROW(deviceInfo = module.getAvailableDevices()); +} + +TEST_F(MqttStreamingClientModuleTest, CreateDeviceConnectionStringNull) +{ + auto module = CreateModule(); + + DevicePtr device; + ASSERT_THROW(device = module.createDevice(nullptr, nullptr), ArgumentNullException); +} + +// TEST_F(MqttStreamingClientModuleTest, CreateDeviceConnectionFailed) +// { +// auto module = CreateModule(); + +// ASSERT_THROW(module.createDevice("daq.mqtt://127.0.0.1", nullptr), CreateFailedException); +// } + +TEST_F(MqttStreamingClientModuleTest, GetAvailableComponentTypes) +{ + const auto module = CreateModule(); + + DictPtr functionBlockTypes; + ASSERT_NO_THROW(functionBlockTypes = module.getAvailableFunctionBlockTypes()); + ASSERT_EQ(functionBlockTypes.getCount(), 0u); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 1u); + ASSERT_TRUE(deviceTypes.hasKey(DaqMqttDeviceTypeId)); + ASSERT_EQ(deviceTypes.get(DaqMqttDeviceTypeId).getId(), DaqMqttDeviceTypeId); + + DictPtr serverTypes; + ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); + ASSERT_EQ(serverTypes.getCount(), 0u); + + // Check module info for module + ModuleInfoPtr moduleInfo; + ASSERT_NO_THROW(moduleInfo = module.getModuleInfo()); + ASSERT_NE(moduleInfo, nullptr); + ASSERT_EQ(moduleInfo.getName(), MODULE_NAME); + ASSERT_EQ(moduleInfo.getId(), MODULE_ID); + + // Check version info for module + VersionInfoPtr versionInfoModule; + ASSERT_NO_THROW(versionInfoModule = moduleInfo.getVersionInfo()); + ASSERT_NE(versionInfoModule, nullptr); + ASSERT_EQ(versionInfoModule.getMajor(), MQTT_STREAM_CLI_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoModule.getMinor(), MQTT_STREAM_CLI_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoModule.getPatch(), MQTT_STREAM_CLI_MODULE_PATCH_VERSION); + + // Check module and version info for device types + for (const auto& deviceType : deviceTypes) + { + ModuleInfoPtr moduleInfoDeviceType; + ASSERT_NO_THROW(moduleInfoDeviceType = deviceType.second.getModuleInfo()); + ASSERT_NE(moduleInfoDeviceType, nullptr); + ASSERT_EQ(moduleInfoDeviceType.getName(), MODULE_NAME); + ASSERT_EQ(moduleInfoDeviceType.getId(), MODULE_ID); + + VersionInfoPtr versionInfoDeviceType; + ASSERT_NO_THROW(versionInfoDeviceType = moduleInfoDeviceType.getVersionInfo()); + ASSERT_NE(versionInfoDeviceType, nullptr); + ASSERT_EQ(versionInfoDeviceType.getMajor(), MQTT_STREAM_CLI_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoDeviceType.getMinor(), MQTT_STREAM_CLI_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoDeviceType.getPatch(), MQTT_STREAM_CLI_MODULE_PATCH_VERSION); + } +} + +TEST_F(MqttStreamingClientModuleTest, DefaultDeviceConfig) +{ + const auto module = CreateModule(); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 1u); + + ASSERT_TRUE(deviceTypes.hasKey(DaqMqttDeviceTypeId)); + auto pseudoDeviceConfig = deviceTypes.get(DaqMqttDeviceTypeId).createDefaultConfig(); + ASSERT_TRUE(pseudoDeviceConfig.assigned()); +} diff --git a/mqtt_streaming_protocol/CMakeLists.txt b/mqtt_streaming_protocol/CMakeLists.txt index e58670a2..ad027558 100644 --- a/mqtt_streaming_protocol/CMakeLists.txt +++ b/mqtt_streaming_protocol/CMakeLists.txt @@ -1,8 +1,13 @@ cmake_minimum_required(VERSION 3.10) -set_cmake_folder_context(TARGET_FOLDER_NAME ${SDK_TARGET_NAMESPACE}_mqtt_streaming_protocol) -project(OpenDaqMqttStreamingProtocol - VERSION 1.0.0 - LANGUAGES CXX -) +set_cmake_folder_context(TARGET_FOLDER_NAME) + +set(MQTT_STREAMING_PROTOCOL_VERSION ${OPENDAQ_PACKAGE_VERSION}) +set(MQTT_STREAMING_PROTOCOL_PRJ_NAME "OpenDaqMqttStreamingProtocol") + +message(STATUS "${MQTT_STREAMING_PROTOCOL_PRJ_NAME} version: ${MQTT_STREAMING_PROTOCOL_VERSION}") +project(${MQTT_STREAMING_PROTOCOL_PRJ_NAME} VERSION ${MQTT_STREAMING_PROTOCOL_VERSION} LANGUAGES C CXX) add_subdirectory(src) +if (OPENDAQ_MQTT_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/mqtt_streaming_protocol/include/IMqttBase.h b/mqtt_streaming_protocol/include/IMqttBase.h deleted file mode 100644 index 14254fee..00000000 --- a/mqtt_streaming_protocol/include/IMqttBase.h +++ /dev/null @@ -1,87 +0,0 @@ -#pragma once -#include -#include -#include "MQTTClientType.h" - -enum class MqttClientType -{ - sync, - async -}; - -namespace mqtt -{ - - enum class MqttConnectionStatus - { - not_connected, - connected, - pending, - }; - -class IMqttBase -{ -public: - IMqttBase(bool enableSSL, - bool useCertificates, - bool verifyServerCert, - std::string trustStorePath, - std::string clientCertPath, - std::string privKeyPath, - std::string privKeyPass) - : enableSSL(enableSSL) - , useCertificates(useCertificates) - , verifyServerCert(verifyServerCert) - , trustStorePath(trustStorePath) - , clientCertPath(clientCertPath) - , privKeyPath(privKeyPath) - , privKeyPass(privKeyPass) - { - } - virtual bool connect() = 0; - virtual bool reconnect() = 0; - virtual bool disconnect() = 0; - void setConnectionStatusCB(std::function cb){ - connCb = cb; - }; - void setOnConnect(std::function cb) { - onConnectCb = cb; - } - virtual void setUsernamePasswrod(std::string username, std::string password) = 0; - virtual void setServerURL(std::string serverUrl) = 0; - virtual std::string getServerUrl() const = 0; - virtual void setClientId(std::string clientId) = 0; - virtual void setSSLConnectionProperties(bool enableSSL, - bool useCertificates, - bool verifyServerCert, - std::string trustStorePath, - std::string clientCertPath, - std::string privKeyPath, - std::string privKeyPass) - { - this->enableSSL = enableSSL; - this->useCertificates = useCertificates; - this->verifyServerCert = verifyServerCert; - this->trustStorePath = trustStorePath; - this->clientCertPath = clientCertPath; - this->privKeyPath = privKeyPath; - this->privKeyPass = privKeyPass; - } - virtual MqttConnectionStatus isConnected() = 0; - virtual MqttClientType getClientType() const = 0; - virtual ~IMqttBase() - { - } - -protected: - bool enableSSL; - bool useCertificates; - bool verifyServerCert; - std::string trustStorePath; - std::string clientCertPath; - std::string privKeyPath; - std::string privKeyPass; - std::function connCb; - std::function onConnectCb; -}; -} // namespace mqtt diff --git a/mqtt_streaming_protocol/include/IMqttPublisher.h b/mqtt_streaming_protocol/include/IMqttPublisher.h deleted file mode 100644 index 0559c2f0..00000000 --- a/mqtt_streaming_protocol/include/IMqttPublisher.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include "IMqttBase.h" -#include - -namespace mqtt -{ -class IMqttPublisher : public virtual IMqttBase -{ -public: - virtual void publishBirthCertificates() = 0; - virtual bool publish(const std::string& topic, void* data, size_t dataLen, std::string* err = nullptr, int qos = 1, int *token = nullptr, bool retained = false) = 0; - virtual void setStateCB(std::function cb) = 0; - virtual void setPublishSuccessCB(std::function cb) - { - this->sentSuccessCb = cb; - } - - virtual void setPublishFailCB(std::function cb) - { - this->sentFailCb = cb; - } - - virtual ~IMqttPublisher() - { - } - -protected: - std::function cb; - std::function sentSuccessCb; - std::function sentFailCb; -}; -} // namespace mqtt diff --git a/mqtt_streaming_protocol/include/IMqttSubscriber.h b/mqtt_streaming_protocol/include/IMqttSubscriber.h deleted file mode 100644 index a2cfdbea..00000000 --- a/mqtt_streaming_protocol/include/IMqttSubscriber.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include "IMqttBase.h" -#include -#include "MqttMessage.h" - -namespace mqtt -{ -class IMqttSubscriber : public virtual IMqttBase -{ -public: - virtual bool subscribe(std::string topic, int qos) = 0; - virtual bool unsubscribe(std::string topic) = 0; - virtual bool unsubscribeAll() = 0; - virtual void setMessageArrivedCb(std::function cb) = 0; - virtual ~IMqttSubscriber() - { - } - -protected: - std::function cb; -}; -} // namespace mqtt diff --git a/mqtt_streaming_protocol/include/MQTTClientType.h b/mqtt_streaming_protocol/include/MQTTClientType.h deleted file mode 100644 index 707254d3..00000000 --- a/mqtt_streaming_protocol/include/MQTTClientType.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -enum class MQTTClientType -{ - publisher, - subscriber, - pubsub, -}; diff --git a/mqtt_streaming_protocol/include/MqttAsyncClient.h b/mqtt_streaming_protocol/include/MqttAsyncClient.h new file mode 100644 index 00000000..d2c32a0c --- /dev/null +++ b/mqtt_streaming_protocol/include/MqttAsyncClient.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include + +#include "MQTTAsync.h" +#include "MqttMessage.h" + +namespace mqtt { + +enum class MqttConnectionStatus { + not_connected, + connected, + pending, +}; + +struct MqttSubscription { + std::string topic; + int qos; + + MqttSubscription(std::string t, int q) + : topic(t) + , qos(q) + {} +}; + +class MqttAsyncClient final { +public: + using MsgArrivedCb_type = void(const MqttAsyncClient &, mqtt::MqttMessage &msg); + + MqttAsyncClient(); + MqttAsyncClient(std::string serverUrl, std::string clientId, bool cleanSession); + MqttAsyncClient(const MqttAsyncClient &) = delete; + MqttAsyncClient &operator=(const MqttAsyncClient &) = delete; + MqttAsyncClient(MqttAsyncClient &&) = delete; + MqttAsyncClient &operator=(MqttAsyncClient &&) = delete; + ~MqttAsyncClient(); + + bool connect(); + bool disconnect(); + bool syncDisconnect(int timeoutMs); + + bool publish(const std::string &topic, + void *data, + size_t dataLen, + std::string *err = nullptr, + int qos = 1, + int *token = nullptr, + bool retained = false); + + bool subscribe(std::string topic, int qos); + bool unsubscribe(std::string topic); + bool unsubscribeAll(); + + void setConnectedCb(std::function cb); + void setMessageArrivedCb(std::string topic, std::function cb); + void setMessageArrivedCb(std::function cb); + void setDisconnectCb(std::function cb); + void setSentCb(std::function cb); + void setDeliveryCompletedCb(std::function cb); + + void setServerURL(std::string serverUrl); + std::string getServerUrl() const; + void setUsernamePasswrod(std::string username, std::string password); + void setClientId(std::string clientId); + MqttConnectionStatus isConnected() const; + +private: + std::string serverUrl; + std::string clientId; + std::string username; + std::string password; + + std::atomic pendingConnect; + + MQTTAsync client; + MQTTAsync_connectOptions connOpts; + MQTTAsync_disconnectOptions disconnOpts; + MQTTAsync_createOptions createOpts; + + std::recursive_mutex cbMtx; + + std::function onConnectedCb; + std::function onSentCb; + std::function onDisconnectCb; + std::function onInternalDisconnectCb; + std::function onDeliveryCompletedCb; + std::function onMsgArrivedCmnCb; + std::unordered_map> onMsgArrivedCbs; + + std::vector subscriptions; + + std::lock_guard getCbLock(); + + static void onDeliveryCompleted(void *context, MQTTAsync_token token); + static void onConnected(void *context, char *cause); + static void onConnectionLost(void *context, char *cause); + static int onMsgArrived(void *context, + char *topicName, + int topicLen, + MQTTAsync_message *message); + static void onSendSuccess(void *context, MQTTAsync_successData *data); + static void onSendFailure(void *context, MQTTAsync_failureData *data); + static void onConnectSuccess(void *context, MQTTAsync_successData *data); + static void onConnectFailure(void *context, MQTTAsync_failureData *data); + static void onDisconnectSuccess(void *context, MQTTAsync_successData *data); + static void onDisconnectFailure(void *context, MQTTAsync_failureData *data); + static void onSubscribeSuccess(void *context, MQTTAsync_successData *response); + static void onSubscribeFailure(void *context, MQTTAsync_failureData *response); + static void onUnsubscribeSuccess(void *context, MQTTAsync_successData *response); + static void onUnsubscribeFailure(void *context, MQTTAsync_failureData *response); +}; +} // namespace mqtt diff --git a/mqtt_streaming_protocol/include/MqttAsyncPublisher.h b/mqtt_streaming_protocol/include/MqttAsyncPublisher.h deleted file mode 100644 index 3c5a6d53..00000000 --- a/mqtt_streaming_protocol/include/MqttAsyncPublisher.h +++ /dev/null @@ -1,167 +0,0 @@ -#pragma once -#include "IMqttPublisher.h" - -#include "MQTTAsync.h" -#include "MqttMessage.h" -#include "MQTTClientType.h" -#include -#include - -namespace mqtt -{ -class MqttAsyncPublisher : public virtual IMqttPublisher -{ -public: - MqttAsyncPublisher(); - MqttAsyncPublisher(std::string serverUrl, - std::string clientId, - bool cleanSession, - bool enableSSL, - bool useCertificates, - bool verifyServerCert, - std::string trustStorePath, - std::string clientCertPath, - std::string privKeyPath, - std::string privKeyPass); - ~MqttAsyncPublisher(); - // Inherited via IMqttSyncPublisher - virtual bool connect() override; - virtual bool disconnect() override; - virtual MqttConnectionStatus isConnected() override; - virtual void setUsernamePasswrod(std::string username, std::string password) override; - virtual void publishBirthCertificates(); - virtual bool publish(const std::string& topic, void* data, size_t dataLen, std::string* err = nullptr, int qos = 1, int* token = nullptr, bool retained = false) override; - virtual void setStateCB(std::function cb) override; - virtual void setServerURL(std::string serverUrl) override; - virtual void setClientId(std::string clientId) override; - virtual bool reconnect() override; - - virtual MqttClientType getClientType() const - { - return MqttClientType::async; - } - - std::string getServerUrl() const override - { - return this->serverUrl; - } - -private: - std::string serverUrl; - std::string clientId; - std::string username; - std::string password; - MQTTAsync client; - MQTTAsync_connectOptions connOpts; - MQTTAsync_createOptions createOpts; - MQTTAsync_SSLOptions ssl_opts = MQTTAsync_SSLOptions_initializer; - std::recursive_mutex mtx; - bool penddingConnect; - - const MQTTClientType type = MQTTClientType::publisher; - - void setPendingConnect(bool penddingConnect) - { - this->penddingConnect = penddingConnect; - } - - void writeConnectionStatusMsg(std::string who, std::string msg, int level) - { - if (this->connCb) - { - this->connCb(who, msg, level, type); - } - } - - static void deliveryComplete(void* context, MQTTAsync_token token) - { - - } - - static void connectedCb(void* context, char* cause) - { - (void) cause; - // Reconnect procedure here! - if (context != nullptr) - { - auto publisher = (MqttAsyncPublisher*) context; - publisher->setPendingConnect(false); - - if (publisher->onConnectCb) - publisher->onConnectCb(); - - MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; - int rc; - opts.onSuccess = MqttAsyncPublisher::onSubscriber; - opts.onFailure = MqttAsyncPublisher::onSubscriberFailure; - opts.context = publisher; - if ((rc = MQTTAsync_subscribe(publisher->client, "STATE", 1, &opts)) != MQTTASYNC_SUCCESS) - { - } - } - } - - static void connlost(void* context, char* cause) - { - (void) cause; - // Reconnect procedure here! - if (context != nullptr) - { - auto publisher = (MqttAsyncPublisher*) context; - publisher->setPendingConnect(false); - } - } - static int msgArrived(void* context, char* topicName, int topicLen, MQTTAsync_message* message) - { - auto publisher = (MqttAsyncPublisher*) context; - if (publisher->cb) - { - publisher->cb(); - } - MQTTAsync_freeMessage(&message); - // if (topicLen > 0) - MQTTAsync_free(topicName); - return 1; - } - - // Inherited via IMqttSyncBase - static void delivered(void* context, MQTTAsync_successData* rsp) - { - } - - static void onSubscriber(void* context, MQTTAsync_successData* response) - { - - } - - static void onSubscriberFailure(void* context, MQTTAsync_failureData* response) - { - - } - - static void onSend(void* context, MQTTAsync_successData* data) - { - auto publisher = (MqttAsyncPublisher*) context; - if (publisher->sentSuccessCb) - publisher->sentSuccessCb(data->token); - } - - static void onSendFailure(void* context, MQTTAsync_failureData* data) - { - auto publisher = (MqttAsyncPublisher*) context; - if (publisher->sentFailCb) - publisher->sentFailCb(data->token); - } - - static void onConnectSuccess(void * context, MQTTAsync_successData data) - { - - } - - static void onConnectFailure(void* context, MQTTAsync_failureData data) - { - auto publisher = (MqttAsyncPublisher*) context; - publisher->setPendingConnect(false); - } -}; -} // namespace mqtt diff --git a/mqtt_streaming_protocol/include/MqttAsyncSubscriber.h b/mqtt_streaming_protocol/include/MqttAsyncSubscriber.h deleted file mode 100644 index f528c024..00000000 --- a/mqtt_streaming_protocol/include/MqttAsyncSubscriber.h +++ /dev/null @@ -1,270 +0,0 @@ -#pragma once - -#include "IMqttSubscriber.h" - -#include "MQTTAsync.h" -#include "MqttMessage.h" -#include "MQTTClientType.h" -#include -#include -#include -#include - -namespace mqtt -{ - -struct MqttSubscription { - std::string topic; - int qos; -}; - -class MqttAsyncSubscriber : public virtual IMqttSubscriber -{ -public: - MqttAsyncSubscriber(); - MqttAsyncSubscriber(std::string serverUrl, - std::string clientId, - bool cleanSession, - bool enableSSL, - bool useCertificates, - bool verifyServerCert, - std::string trustStorePath, - std::string clientCertPath, - std::string privKeyPath, - std::string privKeyPass) - : IMqttBase(enableSSL, useCertificates, verifyServerCert, trustStorePath, clientCertPath, privKeyPath, privKeyPass) - { - this->serverUrl = serverUrl; - this->clientId = clientId; - this->username = ""; - this->password = ""; - this->connOpts = MQTTAsync_connectOptions_initializer; - this->connOpts.cleansession = cleanSession ? 1 : 0; - this->connOpts.keepAliveInterval = 20; - this->connOpts.connectTimeout = 5; - this->connOpts.onSuccess = (MQTTAsync_onSuccess*) &MqttAsyncSubscriber::onConnectSuccess; - this->connOpts.onFailure = (MQTTAsync_onFailure*) &MqttAsyncSubscriber::onConnectFailure; - this->connOpts.automaticReconnect = true; - this->connOpts.minRetryInterval = 1; - this->connOpts.maxRetryInterval = 10; - this->createOpts = MQTTAsync_createOptions_initializer; - this->ssl_opts = MQTTAsync_SSLOptions_initializer; - this->connOpts.ssl = &this->ssl_opts; - - this->willOpts = MQTTAsync_willOptions_initializer; - this->willOpts.topicName = "STATE"; - this->willOpts.qos = 1; - this->willOpts.retained = 1; - this->willOpts.message = "offline"; - this->connOpts.will = &this->willOpts; - this->client = nullptr; - setServerURL(serverUrl); - } - - ~MqttAsyncSubscriber() - { - std::lock_guard guard(mtx); - if (this->client != nullptr) - { - disconnect(); - MQTTAsync_destroy(&this->client); - } - } - - virtual bool connect() override; - virtual bool disconnect() override; - virtual MqttConnectionStatus isConnected() override; - virtual void setServerURL(std::string serverUrl) override; - virtual void setClientId(std::string clientId) override; - virtual void setUsernamePasswrod(std::string username, std::string password); - - virtual MqttClientType getClientType() const - { - return MqttClientType::async; - } - - virtual bool unsubscribeAll() override - { - std::lock_guard guard(mtx); - for (auto& sub : subscriptions) - { - unsubscribe(sub.topic); - } - subscriptions.clear(); - - return true; - } - - virtual bool subscribe(std::string topic, int qos) override - { - std::lock_guard guard(mtx); - MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; - int rc; - opts.onSuccess = MqttAsyncSubscriber::onSubscriber; - opts.onFailure = MqttAsyncSubscriber::onSubscriberFailure; - opts.context = this; - if ((rc = MQTTAsync_subscribe(client, topic.c_str(), qos, &opts)) == MQTTASYNC_SUCCESS) - { - MqttSubscription newSubscription = { topic, qos }; - subscriptions.emplace_back(newSubscription); - } - return rc == MQTTASYNC_SUCCESS; - } - - virtual bool unsubscribe(std::string topic) override - { - std::lock_guard guard(mtx); - MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; - int rc; - opts.onSuccess = MqttAsyncSubscriber::onSubscriber; - opts.onFailure = MqttAsyncSubscriber::onSubscriberFailure; - opts.context = this; - rc = MQTTAsync_unsubscribe(this->client, topic.c_str(), &opts); - auto it = std::find_if(subscriptions.begin(), subscriptions.end(), [&topic](MqttSubscription sub) { return sub.topic == topic; }); - if (it != subscriptions.end()) - subscriptions.erase(it); - return rc == MQTTASYNC_SUCCESS; - } - - virtual void setMessageArrivedCb(std::function cb) override - { - std::lock_guard guard(mtx); - this->cb = cb; - } - - - virtual bool reconnect() override - { - return true; - } - - std::string getClientId() const - { - return this->clientId; - } - - std::string getServerUrl() const override - { - return this->serverUrl; - } - -private: - std::atomic initialConnectionThreadStart = false; - std::string serverUrl; - std::string clientId; - std::string username; - std::string password; - std::vector subscriptions; - std::recursive_mutex mtx; - bool penddingConnect; - const MQTTClientType type = MQTTClientType::subscriber; - - MQTTAsync client; - MQTTAsync_connectOptions connOpts; - MQTTAsync_createOptions createOpts; - MQTTAsync_SSLOptions ssl_opts = MQTTAsync_SSLOptions_initializer; - MQTTAsync_willOptions willOpts; - - void setPendingConnect(bool penddingConnect) - { - this->penddingConnect = penddingConnect; - } - - void writeConnectionStatusMsg(std::string who, std::string msg, int level) - { - if (this->connCb) - { - this->connCb(who, msg, level, type); - } - } - - // Inherited via IMqttSyncBase - - static int msgArrived(void* context, char* topicName, int topicLen, MQTTAsync_message* message) - { - MqttAsyncSubscriber* subscriber = (MqttAsyncSubscriber*) context; - if (subscriber->cb) - { - mqtt::MqttMessage msg; - // Get the topic - msg.setTopic(topicName); - // Copy the payload - msg.addData((uint8_t*) message->payload, message->payloadlen); - - subscriber->cb(*subscriber, msg); - } - - MQTTAsync_freeMessage(&message); - if (topicLen > 0) - MQTTAsync_free(topicName); - return 1; - } - - static void connectedCb(void* context, char* cause) - { - if (context != nullptr) - { - try - { - auto subscriber = (MqttAsyncSubscriber*) context; - subscriber->setPendingConnect(false); - MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; - int rc; - opts.onSuccess = MqttAsyncSubscriber::onSubscriber; - opts.onFailure = MqttAsyncSubscriber::onSubscriberFailure; - opts.context = subscriber; - if (subscriber->onConnectCb) - subscriber->onConnectCb(); - } - catch (std::exception ex) - { - } - } - } - static void connlost(void* context, char* cause) - { - // Reconnect procedure here! - if (context != nullptr) - { - - } - } - - // Inherited via IMqttSyncSubscriber - static void delivered(void* context, MQTTAsync_successData* rsp) - { - } - - static void onSubscriber(void* context, MQTTAsync_successData* response) - { - } - - static void onSubscriberFailure(void* context, MQTTAsync_failureData* response) - { - } - - static void onSend(void* context, MQTTAsync_successData data) - { - } - - static void onSendFailure(void* context, MQTTAsync_failureData data) - { - - } - - static void onConnectSuccess(void* context, MQTTAsync_successData data) - { - // This callback only fires onec when call to MQTTAsync_connect is called. - } - - static void onConnectFailure(void* context, MQTTAsync_failureData data) - { - auto subscriber = (MqttAsyncSubscriber*) context; - subscriber->setPendingConnect(false); - } - - static void deliveryComplete(void* context, MQTTAsync_token token) - { - } -}; -} // namespace mqtt diff --git a/mqtt_streaming_protocol/include/MqttDataWrapper.h b/mqtt_streaming_protocol/include/MqttDataWrapper.h new file mode 100644 index 00000000..8cff6a0d --- /dev/null +++ b/mqtt_streaming_protocol/include/MqttDataWrapper.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace mqtt { + +struct SampleData { + double value; + uint64_t timestamp; +}; + +struct Result { + bool ok; + std::vector msg; +}; + +struct SignalDescriptor { + std::string topic; + std::string name; + std::vector unit; +}; + +class MqttDataWrapper final { +public: + // first = device name, second = list of signal descriptors + using DeviceDescriptorType = std::pair>; + MqttDataWrapper() = delete; + + static std::pair parseSampleData(const std::string& json); + static std::pair parseSignalDescriptors(const std::string& topic, const std::string& json); + + static std::string serializeSampleData(const SampleData& data); + static std::string serializeSignalDescriptors(daq::ListObjectPtr> signals); + + static std::string buildTopicFromId(const std::string& globalId); + static std::string buildSignalsTopic(const std::string& deviceId); +}; +} // namespace mqtt diff --git a/mqtt_streaming_protocol/include/MqttMessage.h b/mqtt_streaming_protocol/include/MqttMessage.h index 8f6726da..df6fe528 100644 --- a/mqtt_streaming_protocol/include/MqttMessage.h +++ b/mqtt_streaming_protocol/include/MqttMessage.h @@ -11,11 +11,25 @@ namespace mqtt class MqttMessage { public: - void setToken(int token) + MqttMessage() = default; + MqttMessage(std::string topic, std::vector data, int qos, bool retained) + : topic(topic) + , data(data) + , qos(qos) + , retained(retained) + {} + bool operator==(const MqttMessage &other) const { - this->token = token; + return topic == other.topic && data == other.data && qos == other.qos; } + bool operator!=(const MqttMessage &other) const + { + return !(*this == other); + } + + void setToken(int token) { this->token = token; } + int getToken() { return token; @@ -37,6 +51,11 @@ class MqttMessage return data; } + const std::vector& getData() const + { + return data; + } + std::string getTopic() const { return topic; diff --git a/mqtt_streaming_protocol/include/MqttSettings.h b/mqtt_streaming_protocol/include/MqttSettings.h new file mode 100644 index 00000000..d8cb04f0 --- /dev/null +++ b/mqtt_streaming_protocol/include/MqttSettings.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace Mqtt::Utils::Settings { +struct MqttConnectionSettings { + std::string mqttUrl; + int port; + std::string username; + std::string password; + std::string clientId; +}; +} diff --git a/mqtt_streaming_protocol/src/CMakeLists.txt b/mqtt_streaming_protocol/src/CMakeLists.txt index bb3c764c..be6d8f43 100644 --- a/mqtt_streaming_protocol/src/CMakeLists.txt +++ b/mqtt_streaming_protocol/src/CMakeLists.txt @@ -1,31 +1,27 @@ -set(BASE_NAME mqtt_streaming_protocol) -set(LIB_NAME ${BASE_NAME}) +set(LIB_NAME mqtt_streaming_protocol) -set(SRC_Cpp MqttAsyncPublisher.cpp - MqttAsyncSubscriber.cpp +set(SRC_Cpp MqttAsyncClient.cpp + MqttDataWrapper.cpp ) -set(SRC_PublicHeaders IMqttBase.h - IMqttPublisher.h - MqttAsyncPublisher.h - IMqttSubscriber.h - MqttAsyncSubscriber.h - MQTTClientType.h +set(SRC_PublicHeaders MqttAsyncClient.h MqttMessage.h + MqttSettings.h + MqttDataWrapper.h ) set(INCLUDE_DIR ../include) prepend_include(${INCLUDE_DIR} SRC_PublicHeaders) +source_group("include" FILES ${SRC_PublicHeaders}) -set(SRC_PrivateHeaders ) +find_package(Boost REQUIRED COMPONENTS algorithm) add_library(${LIB_NAME} STATIC ${SRC_Cpp} ${SRC_PublicHeaders} - ${SRC_PrivateHeaders} ) -add_library(${SDK_TARGET_NAMESPACE}::${BASE_NAME} ALIAS ${LIB_NAME}) +add_library(${SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) if(BUILD_64Bit OR BUILD_ARM) set_target_properties(${LIB_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) @@ -48,8 +44,11 @@ if(PAHO_BUILD_STATIC) set(PAHO_LIB ${PAHO_LIB}-static) endif() -target_link_libraries(${LIB_NAME} PUBLIC ${PAHO_LIB}) -target_link_libraries(${LIB_NAME} PUBLIC rapidjson) +target_link_libraries(${LIB_NAME} PUBLIC ${PAHO_LIB} + daq::opendaq + rapidjson + PRIVATE $ +) if (WIN32) target_link_libraries(${LIB_NAME} PUBLIC diff --git a/mqtt_streaming_protocol/src/MqttAsyncClient.cpp b/mqtt_streaming_protocol/src/MqttAsyncClient.cpp new file mode 100644 index 00000000..ca52a250 --- /dev/null +++ b/mqtt_streaming_protocol/src/MqttAsyncClient.cpp @@ -0,0 +1,451 @@ +#include "MqttAsyncClient.h" +#include + +namespace mqtt +{ + +MqttAsyncClient::MqttAsyncClient() + : MqttAsyncClient("", "", false) +{ +} + +MqttAsyncClient::MqttAsyncClient(std::string serverUrl, std::string clientId, bool cleanSession) + : clientId(clientId), + username(""), + password(""), + pendingConnect(false), + client(nullptr) +{ + setServerURL(serverUrl); + connOpts = MQTTAsync_connectOptions_initializer; + connOpts.cleansession = cleanSession ? 1 : 0; + connOpts.keepAliveInterval = 20; + connOpts.connectTimeout = 5; + connOpts.onSuccess = (MQTTAsync_onSuccess*)&MqttAsyncClient::onConnectSuccess; + connOpts.onFailure = (MQTTAsync_onFailure*)&MqttAsyncClient::onConnectFailure; + connOpts.automaticReconnect = true; + connOpts.minRetryInterval = 1; + connOpts.maxRetryInterval = 10; + connOpts.context = this; + + disconnOpts = MQTTAsync_disconnectOptions_initializer; + disconnOpts.onSuccess = (MQTTAsync_onSuccess*)&MqttAsyncClient::onDisconnectSuccess; + disconnOpts.onFailure = (MQTTAsync_onFailure*)&MqttAsyncClient::onDisconnectFailure; + disconnOpts.context = this; + + createOpts = MQTTAsync_createOptions_initializer; +} + +MqttAsyncClient::~MqttAsyncClient() +{ + if (client != nullptr) + { + MQTTAsync_destroy(&client); + } +} + +bool MqttAsyncClient::connect() +{ + if (client != nullptr) + { + MQTTAsync_destroy(&client); + } + + if (serverUrl.empty() || clientId.empty()) + { + return false; + } + + int rc = MQTTAsync_createWithOptions(&client, serverUrl.c_str(), clientId.c_str(), MQTTCLIENT_PERSISTENCE_NONE, NULL, &createOpts); + + if (rc != MQTTASYNC_SUCCESS) + { + return false; + } + + rc = MQTTAsync_setCallbacks(client, + this, + &MqttAsyncClient::onConnectionLost, + &MqttAsyncClient::onMsgArrived, + &MqttAsyncClient::onDeliveryCompleted); + + if (rc != MQTTASYNC_SUCCESS) + { + return false; + } + + rc = MQTTAsync_setConnected(client, this, &MqttAsyncClient::onConnected); + if (rc != MQTTASYNC_SUCCESS) + { + return false; + } + + rc = MQTTAsync_connect(client, &connOpts); + if (rc != MQTTASYNC_SUCCESS) + { + return false; + } + pendingConnect = true; + return true; +} + +bool MqttAsyncClient::disconnect() +{ + if (client == nullptr) + return true; + + // It is only the result of the request to disconnect (queuing) + auto status = MQTTAsync_disconnect(client, &disconnOpts); + bool result = (status == MQTTASYNC_SUCCESS || status == MQTTASYNC_DISCONNECTED); + return result; +} + +bool MqttAsyncClient::syncDisconnect(int timeoutMs) +{ + if (client == nullptr) + return true; + + bool result = disconnect(); + if (result) + { + std::atomic done{false}; + std::promise disconnectedPromise; + auto disconnectedFuture = disconnectedPromise.get_future(); + { + auto lock = getCbLock(); + onInternalDisconnectCb = [promise = &disconnectedPromise, &done](bool result) { + bool expected = false; + if (done.compare_exchange_strong(expected, true)) { + promise->set_value(result); + } + }; + } + auto status = disconnectedFuture.wait_for(std::chrono::milliseconds(timeoutMs)); + { + auto lock = getCbLock(); + onInternalDisconnectCb = nullptr; + } + if (status == std::future_status::ready && disconnectedFuture.get() == true) + { + MQTTAsync_destroy(&client); + return true; + } + } + return false; +} + +MqttConnectionStatus MqttAsyncClient::isConnected() const +{ + if (pendingConnect) + return MqttConnectionStatus::pending; + + return MQTTAsync_isConnected(client) ? MqttConnectionStatus::connected : MqttConnectionStatus::not_connected; +} + +std::lock_guard MqttAsyncClient::getCbLock() +{ + return std::lock_guard(cbMtx); +} + +void MqttAsyncClient::setUsernamePasswrod(std::string username, std::string password) +{ + this->username = username; + this->password = password; + + connOpts.username = !username.empty() ? username.c_str() : NULL; + connOpts.password = !password.empty() ? password.c_str() : NULL; +} + +bool MqttAsyncClient::publish(const std::string& topic, void* data, size_t dataLen, std::string* err, int qos, int* token, bool retained) +{ + std::string tmpErr; + if (client == nullptr) + { + tmpErr = "MQTTAsync is nullptr"; + } + + if (topic.empty()) + { + tmpErr = "topic is empty"; + } + + if (qos > 2 || qos < 0) + { + tmpErr = "QoS is wrong"; + } + + if (!tmpErr.empty()) + { + if (err != nullptr) + { + *err = std::move(tmpErr); + } + return false; + } + + MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; + MQTTAsync_message pubmsg = MQTTAsync_message_initializer; + int rc; + opts.onSuccess = (MQTTAsync_onSuccess*)&MqttAsyncClient::onSendSuccess; + opts.onFailure = (MQTTAsync_onFailure*)&MqttAsyncClient::onSendFailure; + opts.context = this; + pubmsg.payload = data; + pubmsg.payloadlen = (int)dataLen; + pubmsg.qos = qos; + pubmsg.retained = retained ? 1 : 0; + if ((rc = MQTTAsync_sendMessage(client, topic.c_str(), &pubmsg, &opts)) != MQTTASYNC_SUCCESS) + { + if (err != nullptr) + { + *err = MQTTAsync_strerror(rc); + } + } + if (token != nullptr) + { + *token = opts.token; + } + return rc == MQTTASYNC_SUCCESS; +} + +bool MqttAsyncClient::subscribe(std::string topic, int qos) +{ + MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; + int rc; + opts.onSuccess = MqttAsyncClient::onSubscribeSuccess; + opts.onFailure = MqttAsyncClient::onSubscribeFailure; + opts.context = this; + if ((rc = MQTTAsync_subscribe(client, topic.c_str(), qos, &opts)) == MQTTASYNC_SUCCESS) + { + subscriptions.emplace_back(topic, qos); + } + return rc == MQTTASYNC_SUCCESS; +} + +bool MqttAsyncClient::unsubscribe(std::string topic) +{ + MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; + opts.onSuccess = (MQTTAsync_onSuccess*)&MqttAsyncClient::onUnsubscribeSuccess; + opts.onFailure = (MQTTAsync_onFailure*)&MqttAsyncClient::onUnsubscribeFailure; + opts.context = this; + int rc = MQTTAsync_unsubscribe(client, topic.c_str(), &opts); + auto it = + std::find_if(subscriptions.begin(), subscriptions.end(), [&topic](const MqttSubscription& sub) { return sub.topic == topic; }); + if (it != subscriptions.end()) + subscriptions.erase(it); + return rc == MQTTASYNC_SUCCESS; +} + +bool MqttAsyncClient::unsubscribeAll() +{ + for (auto& sub : subscriptions) + { + unsubscribe(sub.topic); + } + subscriptions.clear(); + + return true; +} + +void MqttAsyncClient::setServerURL(std::string serverUrl) +{ + if (serverUrl != "") + { + serverUrl = "tcp://" + serverUrl; + } + this->serverUrl = serverUrl; +} + +void MqttAsyncClient::setClientId(std::string clientId) +{ + this->clientId = clientId; +} + +std::string MqttAsyncClient::getServerUrl() const +{ + return serverUrl; +} + +void MqttAsyncClient::onDeliveryCompleted(void* context, MQTTAsync_token token) +{ + if (context != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + auto lock = clienttInst->getCbLock(); + if (clienttInst->onDeliveryCompletedCb) + clienttInst->onDeliveryCompletedCb(token); + } +} + +void MqttAsyncClient::onConnected(void* context, char* cause) +{ + if (context != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + clienttInst->pendingConnect = false; + auto lock = clienttInst->getCbLock(); + if (clienttInst->onConnectedCb) + clienttInst->onConnectedCb(); + } +} + +void MqttAsyncClient::onConnectionLost(void* context, char* cause) +{ + (void)cause; + // Reconnect procedure here! + if (context != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + clienttInst->pendingConnect = false; + } +} + +int MqttAsyncClient::onMsgArrived(void* context, char* topicName, int topicLen, MQTTAsync_message* message) +{ + if (context != nullptr && message != nullptr) + { + MqttAsyncClient* client = (MqttAsyncClient*)context; + { + auto lock = client->getCbLock(); + auto it = (topicName != nullptr) ? client->onMsgArrivedCbs.find(topicName) : client->onMsgArrivedCbs.end(); + if (client->onMsgArrivedCmnCb || it != client->onMsgArrivedCbs.end()) + { + mqtt::MqttMessage msg; + msg.setTopic(topicName); + msg.addData((uint8_t*)message->payload, message->payloadlen); + msg.setQos(message->qos); + msg.setRetained(message->retained != 0); + if (client->onMsgArrivedCmnCb) + client->onMsgArrivedCmnCb(*client, msg); + if (it != client->onMsgArrivedCbs.end() && it->second) + it->second(*client, msg); + } + } + } + if (message != nullptr) + MQTTAsync_freeMessage(&message); + MQTTAsync_free(topicName); + return 1; +} + +void MqttAsyncClient::onSendSuccess(void* context, MQTTAsync_successData* data) +{ + if (context != nullptr && data != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + auto lock = clienttInst->getCbLock(); + if (clienttInst->onSentCb) + clienttInst->onSentCb(data->token, true); + } +} + +void MqttAsyncClient::onSendFailure(void* context, MQTTAsync_failureData* data) +{ + if (context != nullptr && data != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + auto lock = clienttInst->getCbLock(); + if (clienttInst->onSentCb) + clienttInst->onSentCb(data->token, false); + } +} + +void MqttAsyncClient::onConnectSuccess(void* context, MQTTAsync_successData* data) +{ + // TODO : check when this is called + if (context != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + clienttInst->pendingConnect = false; + } +} + +void MqttAsyncClient::onConnectFailure(void* context, MQTTAsync_failureData* data) +{ + // TODO : check when this is called + if (context != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + clienttInst->pendingConnect = false; + } +} + +void MqttAsyncClient::onDisconnectSuccess(void* context, MQTTAsync_successData* data) +{ + // TODO : check when this is called + if (context != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + auto lock = clienttInst->getCbLock(); + if (clienttInst->onInternalDisconnectCb) + clienttInst->onInternalDisconnectCb(true); + if (clienttInst->onDisconnectCb) + clienttInst->onDisconnectCb(true); + } +} + +void MqttAsyncClient::onDisconnectFailure(void* context, MQTTAsync_failureData* data) +{ + // TODO : check when this is called + if (context != nullptr) + { + auto clienttInst = (MqttAsyncClient*)context; + auto lock = clienttInst->getCbLock(); + if (clienttInst->onInternalDisconnectCb) + clienttInst->onInternalDisconnectCb(false); + if (clienttInst->onDisconnectCb) + clienttInst->onDisconnectCb(false); + } +} + +void MqttAsyncClient::onSubscribeSuccess(void* context, MQTTAsync_successData* response) +{ +} + +void MqttAsyncClient::onSubscribeFailure(void* context, MQTTAsync_failureData* response) +{ +} + +void MqttAsyncClient::onUnsubscribeSuccess(void* context, MQTTAsync_successData* response) +{ +} + +void MqttAsyncClient::onUnsubscribeFailure(void* context, MQTTAsync_failureData* response) +{ +} + +void MqttAsyncClient::setConnectedCb(std::function cb) +{ + auto lock = getCbLock(); + onConnectedCb = cb; +} + +void MqttAsyncClient::setMessageArrivedCb(std::string topic, std::function cb) +{ + auto lock = getCbLock(); + onMsgArrivedCbs.insert({topic, cb}); +} + +void MqttAsyncClient::setMessageArrivedCb(std::function cb) +{ + auto lock = getCbLock(); + onMsgArrivedCmnCb = cb; +} + +void MqttAsyncClient::setDisconnectCb(std::function cb) +{ + auto lock = getCbLock(); + onDisconnectCb = cb; +} + +void MqttAsyncClient::setSentCb(std::function cb) +{ + auto lock = getCbLock(); + onSentCb = cb; +} + +void MqttAsyncClient::setDeliveryCompletedCb(std::function cb) +{ + auto lock = getCbLock(); + onDeliveryCompletedCb = cb; +} +} // namespace mqtt diff --git a/mqtt_streaming_protocol/src/MqttAsyncPublisher.cpp b/mqtt_streaming_protocol/src/MqttAsyncPublisher.cpp deleted file mode 100644 index 6b637703..00000000 --- a/mqtt_streaming_protocol/src/MqttAsyncPublisher.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "MqttAsyncPublisher.h" - -#include - -namespace mqtt { - -MqttAsyncPublisher::MqttAsyncPublisher() : - MqttAsyncPublisher("", "", false, false, false, false, "", "", "", "") -{ - -} - -MqttAsyncPublisher::MqttAsyncPublisher(std::string serverUrl, - std::string clientId, - bool cleanSession, - bool enableSSL, - bool useCertificates, - bool verifyServerCert, - std::string trustStorePath, - std::string clientCertPath, - std::string privKeyPath, - std::string privKeyPass) - : IMqttBase(enableSSL, useCertificates, verifyServerCert, trustStorePath, clientCertPath, privKeyPath, privKeyPass) - , penddingConnect(false) -{ - this->serverUrl = serverUrl; - this->clientId = clientId; - this->username = ""; - this->password = ""; - this->connOpts = MQTTAsync_connectOptions_initializer; - this->connOpts.cleansession = cleanSession ? 1 : 0; - this->connOpts.keepAliveInterval = 20; - this->connOpts.connectTimeout = 5; - this->connOpts.onSuccess = (MQTTAsync_onSuccess*) &MqttAsyncPublisher::onConnectSuccess; - this->connOpts.onFailure = (MQTTAsync_onFailure*) &MqttAsyncPublisher::onConnectFailure; - this->connOpts.automaticReconnect = true; - this->connOpts.minRetryInterval = 1; - this->connOpts.maxRetryInterval = 10; - this->createOpts = MQTTAsync_createOptions_initializer; - this->ssl_opts = MQTTAsync_SSLOptions_initializer; - this->connOpts.ssl = &this->ssl_opts; - this->client = nullptr; - setServerURL(serverUrl); -} - -MqttAsyncPublisher::~MqttAsyncPublisher() -{ - std::lock_guard guard(mtx); - if (this->client != nullptr) - { - disconnect(); - MQTTAsync_destroy(&this->client); - } -} - -bool MqttAsyncPublisher::connect() -{ - std::lock_guard guard(mtx); - if (this->client != nullptr) - { - // Signal stop reconnecting - disconnect(); - MQTTAsync_destroy(&this->client); - } - - penddingConnect = true; - setServerURL(this->serverUrl); - int rc = MQTTAsync_createWithOptions(&this->client, this->serverUrl.c_str(), this->clientId.c_str(), MQTTCLIENT_PERSISTENCE_NONE, NULL, &this->createOpts); - - if (rc != MQTTASYNC_SUCCESS) - { - return false; - } - - rc = MQTTAsync_setCallbacks(this->client, this, &MqttAsyncPublisher::connlost, &MqttAsyncPublisher::msgArrived, &MqttAsyncPublisher::deliveryComplete); - if (rc != MQTTASYNC_SUCCESS) - { - return false; - } - - rc = MQTTAsync_setConnected(this->client, this, &MqttAsyncPublisher::connectedCb); - if (rc != MQTTASYNC_SUCCESS) - { - return false; - } - - this->connOpts.context = this; - if ((rc = MQTTAsync_connect(this->client, &this->connOpts)) != MQTTASYNC_SUCCESS) - { - return false; - } - - - return true; -} - -bool MqttAsyncPublisher::disconnect() -{ - std::lock_guard guard(mtx); - return MQTTAsync_disconnect(this->client, NULL) == MQTTASYNC_SUCCESS; -} - -MqttConnectionStatus MqttAsyncPublisher::isConnected() -{ - std::lock_guard guard(mtx); - if (this->penddingConnect) - return MqttConnectionStatus::pending; - return MQTTAsync_isConnected(this->client) ? MqttConnectionStatus::connected : MqttConnectionStatus::not_connected; -} - -void MqttAsyncPublisher::setUsernamePasswrod(std::string username, std::string password) -{ - std::lock_guard guard(mtx); - this->username = username; - this->password = password; - - this->connOpts.username = !username.empty() ? this->username.c_str() : NULL; - this->connOpts.password = !password.empty() ? this->password.c_str() : NULL; -} - -void MqttAsyncPublisher::publishBirthCertificates() -{ -} - -bool MqttAsyncPublisher::publish(const std::string& topic, void* data, size_t dataLen, std::string* err, int qos, int* token, bool retained) -{ - std::lock_guard guard(mtx); - MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer; - MQTTAsync_message pubmsg = MQTTAsync_message_initializer; - int rc; - opts.onSuccess = (MQTTAsync_onSuccess*) &MqttAsyncPublisher::onSend; - opts.onFailure = (MQTTAsync_onFailure*) &MqttAsyncPublisher::onSendFailure; - opts.context = this; - pubmsg.payload = data; - pubmsg.payloadlen = (int) dataLen; - pubmsg.qos = qos; - pubmsg.retained = retained ? 1 : 0; - if ((rc = MQTTAsync_sendMessage(client, topic.c_str(), &pubmsg, &opts)) != MQTTASYNC_SUCCESS) - { - if (err != nullptr) - { - *err = MQTTAsync_strerror(rc); - } - } - if (token != nullptr) - { - *token = opts.token; - } - return rc == MQTTASYNC_SUCCESS; -} - -void MqttAsyncPublisher::setStateCB(std::function cb) -{ - this->cb = cb; -} - -void MqttAsyncPublisher::setServerURL(std::string serverUrl) -{ - std::lock_guard guard(mtx); - this->serverUrl = serverUrl; - if (serverUrl[0] == ':' || serverUrl == "") - { - this->serverUrl = ""; - return; - } - std::string ssl("ssl://"); - std::string tcp("tcp://"); - // Remove any protocol if available - if ((this->serverUrl.size() >= ssl.size() && std::equal(ssl.begin(), ssl.end(), this->serverUrl.begin()))) - { - this->serverUrl.erase(0, ssl.length()); - } - if ((this->serverUrl.size() >= tcp.size() && std::equal(tcp.begin(), tcp.end(), this->serverUrl.begin()))) - { - this->serverUrl.erase(0, tcp.length()); - } - - if (enableSSL) - { - this->serverUrl = "ssl://" + this->serverUrl; - this->ssl_opts.enableServerCertAuth = this->useCertificates && this->verifyServerCert && trustStorePath != ""; - if (trustStorePath != "") - { - this->ssl_opts.trustStore = trustStorePath.c_str(); - } - else - { - this->ssl_opts.trustStore = nullptr; - } - if (this->useCertificates) - { - this->ssl_opts.keyStore = clientCertPath.c_str(); - this->ssl_opts.privateKey = privKeyPath.c_str(); - } - else - { - this->ssl_opts.keyStore = nullptr; - this->ssl_opts.privateKey = nullptr; - } - } - else - { - this->serverUrl = "tcp://" + this->serverUrl; - } -} - -void MqttAsyncPublisher::setClientId(std::string clientId) -{ - std::lock_guard guard(mtx); - this->clientId = clientId; -} -bool MqttAsyncPublisher::reconnect() -{ - return false; -} -} diff --git a/mqtt_streaming_protocol/src/MqttAsyncSubscriber.cpp b/mqtt_streaming_protocol/src/MqttAsyncSubscriber.cpp deleted file mode 100644 index 491c3f24..00000000 --- a/mqtt_streaming_protocol/src/MqttAsyncSubscriber.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#include "MqttAsyncSubscriber.h" - -namespace mqtt { - MqttAsyncSubscriber::MqttAsyncSubscriber() : - MqttAsyncSubscriber("", "", false, false, false, false, "", "", "", "") - { - } - - bool MqttAsyncSubscriber::connect() -{ - std::lock_guard guard(mtx); - if (this->client != nullptr) - { - // Signal stop reconnecting - disconnect(); - MQTTAsync_destroy(&this->client); - } - - penddingConnect = true; - setServerURL(this->serverUrl); - int rc = MQTTAsync_createWithOptions( - &this->client, this->serverUrl.c_str(), this->clientId.c_str(), MQTTCLIENT_PERSISTENCE_NONE, NULL, &this->createOpts); - - if (rc != MQTTASYNC_SUCCESS) - { - return false; - } - - rc = MQTTAsync_setCallbacks( - this->client, this, &MqttAsyncSubscriber::connlost, &MqttAsyncSubscriber::msgArrived, &MqttAsyncSubscriber::deliveryComplete); - if (rc != MQTTASYNC_SUCCESS) - { - return false; - } - - rc = MQTTAsync_setConnected(this->client, this, &MqttAsyncSubscriber::connectedCb); - if (rc != MQTTASYNC_SUCCESS) - { - return false; - } - - this->connOpts.context = this; - if ((rc = MQTTAsync_connect(this->client, &this->connOpts)) != MQTTASYNC_SUCCESS) - { - return false; - } - - return true; -} - -bool MqttAsyncSubscriber::disconnect() -{ - std::lock_guard guard(mtx); - return MQTTAsync_disconnect(this->client, NULL) == MQTTASYNC_SUCCESS; -} - -MqttConnectionStatus MqttAsyncSubscriber::isConnected() -{ - std::lock_guard guard(mtx); - if (this->penddingConnect) - return MqttConnectionStatus::pending; - return MQTTAsync_isConnected(this->client) ? MqttConnectionStatus::connected : MqttConnectionStatus::not_connected; -} - -void MqttAsyncSubscriber::setServerURL(std::string serverUrl) -{ - std::lock_guard guard(mtx); - this->serverUrl = serverUrl; - if (serverUrl[0] == ':' || serverUrl == "") - { - this->serverUrl = ""; - return; - } - std::string ssl("ssl://"); - std::string tcp("tcp://"); - // Remove any protocol if available - if ((this->serverUrl.size() >= ssl.size() && std::equal(ssl.begin(), ssl.end(), this->serverUrl.begin()))) - { - this->serverUrl.erase(0, ssl.length()); - } - if ((this->serverUrl.size() >= tcp.size() && std::equal(tcp.begin(), tcp.end(), this->serverUrl.begin()))) - { - this->serverUrl.erase(0, tcp.length()); - } - - if (enableSSL) - { - this->serverUrl = "ssl://" + this->serverUrl; - this->ssl_opts.enableServerCertAuth = this->useCertificates && this->verifyServerCert && trustStorePath != ""; - if (trustStorePath != "") - { - this->ssl_opts.trustStore = trustStorePath.c_str(); - } - else - { - this->ssl_opts.trustStore = nullptr; - } - if (this->useCertificates) - { - this->ssl_opts.keyStore = clientCertPath.c_str(); - this->ssl_opts.privateKey = privKeyPath.c_str(); - } - else - { - this->ssl_opts.keyStore = nullptr; - this->ssl_opts.privateKey = nullptr; - } - } - else - { - this->serverUrl = "tcp://" + this->serverUrl; - } -} - -void MqttAsyncSubscriber::setClientId(std::string clientId) -{ - std::lock_guard guard(mtx); - this->clientId = clientId; -} - -void MqttAsyncSubscriber::setUsernamePasswrod(std::string username, std::string password) -{ - std::lock_guard guard(mtx); - this->username = username; - this->password = password; - - this->connOpts.username = !username.empty() ? this->username.c_str() : NULL; - this->connOpts.password = !password.empty() ? this->password.c_str() : NULL; -} -} diff --git a/mqtt_streaming_protocol/src/MqttDataWrapper.cpp b/mqtt_streaming_protocol/src/MqttDataWrapper.cpp new file mode 100644 index 00000000..fca11201 --- /dev/null +++ b/mqtt_streaming_protocol/src/MqttDataWrapper.cpp @@ -0,0 +1,199 @@ +#include "MqttDataWrapper.h" + +#include +#include +#include +#include "rapidjson/writer.h" + +namespace mqtt { + +static const char* TOPIC_ALL_SIGNALS_PREFIX = "openDAQ"; +static const char* DEVICE_SIGNAL_LIST = "$signals"; + +std::pair MqttDataWrapper::parseSampleData(const std::string &json) +{ + std::pair res{{false, {}}, {0.0, 0}}; + Result& status = res.first; + SampleData& data = res.second; + try { + rapidjson::Document jsonDocument; + jsonDocument.Parse(json); + if (jsonDocument.HasParseError()) { + status.msg.emplace_back("Error parsing mqtt payload as JSON"); + return res; + } + + if (jsonDocument.IsObject()) { + + int successCnt = 0; + for (auto it = jsonDocument.MemberBegin(); it != jsonDocument.MemberEnd(); ++it) { + const std::string name = it->name.GetString(); + if (name == "value") { + if (jsonDocument[name].IsDouble() || jsonDocument[name].IsInt() || jsonDocument[name].IsFloat()){ + data.value = jsonDocument[name].GetDouble(); + successCnt++; + } else { + status.msg.emplace_back("Value is not supported."); + } + } else if (name == "timestamp") { + if (jsonDocument[name].IsInt() || jsonDocument[name].IsUint64() || jsonDocument[name].IsInt64()){ + data.timestamp = jsonDocument[name].GetUint64(); + successCnt++; + } else { + status.msg.emplace_back("Value is not supported."); + } + } else { + status.msg.emplace_back(fmt::format("Field \"{}\" is not supported.", name)); + } + } + if (successCnt == 2) { + status.ok = true; + } else { + status.msg.emplace_back("Not all required fields are present."); + + } + } + } + catch (...) { + status.msg.emplace_back("Error deserializing mqtt payload"); + } + return res; +} + +std::string MqttDataWrapper::serializeSampleData(const SampleData &data) +{ + std::string result; + + rapidjson::Document doc; + doc.SetObject(); + doc.AddMember("value", rapidjson::Value(data.value), doc.GetAllocator()); + doc.AddMember("timestamp", rapidjson::Value(data.timestamp), doc.GetAllocator()); + + // Serialize to string + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + doc.Accept(writer); + + result = buffer.GetString(); + + return result; +} + +std::string MqttDataWrapper::serializeSignalDescriptors( + daq::ListObjectPtr > signals) +{ + std::string result; + rapidjson::Document doc; + doc.SetArray(); + + auto& allocator = doc.GetAllocator(); + + for (const auto& signal : signals) { + rapidjson::Value obj(rapidjson::kObjectType); + + // topic + { + std::string topic = buildTopicFromId(signal.getGlobalId().toStdString()); + obj.AddMember("topic", rapidjson::Value(topic.c_str(), allocator), allocator); + } + + // name + { + std::string name = ""; + if (signal.getName().assigned()) + name = signal.getName().toStdString(); + obj.AddMember("name", rapidjson::Value(name.c_str(), allocator), allocator); + } + + // unit + { + auto unit = signal.getDescriptor().getUnit(); + rapidjson::Value unitArray(rapidjson::kArrayType); + + if (unit.assigned()) { + auto addUnitInfo = [&unitArray, &allocator](daq::StringPtr unitInfo) { + if (unitInfo.assigned()) + unitArray.PushBack(rapidjson::Value(unitInfo.toStdString().c_str(), allocator), allocator); + }; + addUnitInfo(unit.getSymbol()); + addUnitInfo(unit.getName()); + addUnitInfo(unit.getQuantity()); + } + obj.AddMember("unit", unitArray, allocator); + } + + doc.PushBack(obj, allocator); + } + + // Serialize to string + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + doc.Accept(writer); + + result = buffer.GetString(); + return result; +} + +std::pair +MqttDataWrapper::parseSignalDescriptors(const std::string& topic, const std::string &json) +{ + std::pair res{{false, {}}, {"", {}}}; + Result& status = res.first; + auto& [deviceName, signalDesc] = res.second; + { + std::vector list; + boost::split(list, topic, boost::is_any_of("/")); + + if (list.size() != 3 + || list[0] != TOPIC_ALL_SIGNALS_PREFIX + || list[2] != DEVICE_SIGNAL_LIST) { + return res; // not a signal list message + } + + deviceName = list[1]; + } + + rapidjson::Document doc; + + if (doc.Parse(json.c_str()).HasParseError() || !doc.IsArray()) { + status.msg.emplace_back(fmt::format("{} - Signal list format is not correct: {}", topic, json)); + return res; + } + + const auto array = doc.GetArray(); + + signalDesc.reserve(array.Size()); + for (const auto& v : array) { + if (v.IsObject()) { + SignalDescriptor sd; + if (v.HasMember("topic") && v["topic"].IsString()) { + sd.topic = v["topic"].GetString(); + } + + if (v.HasMember("name") && v["name"].IsString()) { + sd.name = v["name"].GetString(); + } + + if (v.HasMember("unit") && v["unit"].IsArray()) { + for (auto& u : v["unit"].GetArray()) { + if (u.IsString()) + sd.unit.emplace_back(u.GetString()); + } + } + signalDesc.emplace_back(std::move(sd)); + } + } + status.ok = true; + return res; +} + +std::string MqttDataWrapper::buildTopicFromId(const std::string& globalId) +{ + return (TOPIC_ALL_SIGNALS_PREFIX + globalId); +} + +std::string MqttDataWrapper::buildSignalsTopic(const std::string& deviceId) +{ + return (TOPIC_ALL_SIGNALS_PREFIX + deviceId + "/" + DEVICE_SIGNAL_LIST); +} +} diff --git a/mqtt_streaming_protocol/tests/CMakeLists.txt b/mqtt_streaming_protocol/tests/CMakeLists.txt new file mode 100644 index 00000000..4c0dd569 --- /dev/null +++ b/mqtt_streaming_protocol/tests/CMakeLists.txt @@ -0,0 +1,18 @@ +set(MODULE_NAME mqtt_streaming_protocol) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_mqtt_streaming_protocol.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + ${MODULE_NAME} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) diff --git a/mqtt_streaming_protocol/tests/test_app.cpp b/mqtt_streaming_protocol/tests/test_app.cpp new file mode 100644 index 00000000..d92f41f2 --- /dev/null +++ b/mqtt_streaming_protocol/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/mqtt_streaming_protocol/tests/test_mqtt_streaming_protocol.cpp b/mqtt_streaming_protocol/tests/test_mqtt_streaming_protocol.cpp new file mode 100644 index 00000000..95c16fe9 --- /dev/null +++ b/mqtt_streaming_protocol/tests/test_mqtt_streaming_protocol.cpp @@ -0,0 +1,421 @@ +#include "MqttAsyncClient.h" +#include +#include +#include +#include + +using namespace mqtt; +using namespace std::chrono; + +class Timer +{ +public: + Timer(int ms) + { + start = steady_clock::now(); + timeout = milliseconds(ms); + } + milliseconds remain() const { + auto now = steady_clock::now(); + const auto elapsed_ms = std::chrono::duration_cast(now - start); + milliseconds newTout = (elapsed_ms >= timeout) ? milliseconds(0) : timeout - elapsed_ms; + return newTout; + } + bool expired() { + return remain() == milliseconds(0); + } + explicit operator milliseconds() const noexcept + { + return remain(); + } +protected: + std::chrono::steady_clock::time_point start; + std::chrono::milliseconds timeout; +}; + +class MqttAsyncClientWrapper +{ +public: + MqttAsyncClientWrapper() = default; + MqttAsyncClientWrapper(std::shared_ptr instance, std::string clientId = "") + : instance(instance) + { + if (!clientId.empty()) { + this->clientId = clientId; + } + } + bool createConnection(const std::string& url, const std::string& id) { + instance->setServerURL(url); + instance->setClientId(id); + + connectedDone = false; + connectedPromise = std::promise(); + connectedFuture = connectedPromise.get_future(); + + instance->setConnectedCb([this]() { + bool expected = false; + if (connectedDone.compare_exchange_strong(expected, true)) { + connectedPromise.set_value(true); + } + }); + return instance->connect(); + } + + bool connect(const std::string& url) { + return connect(url, clientId); + } + bool connect(const std::string& url, const std::string& id) { + bool res = createConnection(url, id); + if (res) { + auto status = connectedFuture.wait_for(milliseconds(successTimeout)); + instance->setConnectedCb(nullptr); + res = (status == std::future_status::ready && connectedFuture.get() == true); + } + return res; + } + + bool disconnect() { + if (instance->isConnected() != MqttConnectionStatus::connected) { + return true; + } + std::atomic done{false}; + std::promise disconnectedPromise; + auto disconnectedFuture = disconnectedPromise.get_future(); + instance->setDisconnectCb([promise = &disconnectedPromise, &done](bool result) { + bool expected = false; + if (done.compare_exchange_strong(expected, true)) { + promise->set_value(result); + } + }); + + auto disconnectionOk = instance->disconnect(); + if (!disconnectionOk) { + return false; + } + + auto status = disconnectedFuture.wait_for(milliseconds(successTimeout)); + instance->setDisconnectCb(nullptr); + return (status == std::future_status::ready && disconnectedFuture.get() == true); + } + + bool removeRetainedTopic(const std::string& topic) { + return publishMsg(topic, "", true); + } + + bool publishMsg(const std::string& topic, const std::string& data, bool retained = false) { + const MqttMessage msg(topic, std::vector(data.begin(), data.end()), 1, retained); + return publishMsg(msg); + } + + bool publishMsg(const MqttMessage& msg) { + int token = 0; + + std::promise deliveryPromise; + auto deliveryFuture = deliveryPromise.get_future(); + instance->setDeliveryCompletedCb([promise = &deliveryPromise](int deliveredToken) { + promise->set_value(deliveredToken); + }); + + Timer sendTimer(successTimeout); + auto ok = instance->publish(msg.getTopic(), + (void *) (msg.getData().data()), + msg.getData().size(), + nullptr, + msg.getQos(), + &token, + msg.getRetained()); + if (!ok || token == 0) { + instance->setDeliveryCompletedCb(nullptr); + return false; + } + + auto status = deliveryFuture.wait_for(sendTimer.remain()); + instance->setDeliveryCompletedCb(nullptr); + return (status == std::future_status::ready && deliveryFuture.get() == token); + } + + std::string buildTopicName() { + return std::string("test/topic/") + std::string(::testing::UnitTest::GetInstance()->current_test_info()->name()); + } + + std::shared_ptr instance; + std::promise connectedPromise; + std::future connectedFuture; + std::atomic connectedDone{false}; + + int successTimeout = 5000; + int failureTimeout = 3000; + std::string clientId = "testMqttClientId"; +}; + +class MqttStreamingProtocolTest : public ::testing::Test, public MqttAsyncClientWrapper { +protected: + void SetUp() override { + instance = std::make_shared(); + } + + void TearDown() override { + instance.reset(); + } +}; + +TEST_F(MqttStreamingProtocolTest, Connection) +{ + auto ok = createConnection("127.0.0.1", clientId); + ASSERT_TRUE(ok); + + auto status = connectedFuture.wait_for(Timer(successTimeout).remain()); + instance->setConnectedCb(nullptr); + ASSERT_TRUE(status == std::future_status::ready); + ASSERT_TRUE(connectedFuture.get()); +} + +TEST_F(MqttStreamingProtocolTest, Reconnection) +{ + auto ok = connect("127.0.0.1", clientId); + ASSERT_TRUE(ok); + + ok = connect("127.0.0.1", clientId); + ASSERT_TRUE(ok); +} + +TEST_F(MqttStreamingProtocolTest, WrongUrlConnection) +{ + auto ok = createConnection("", clientId); + ASSERT_FALSE(ok); + + auto status = connectedFuture.wait_for(Timer(failureTimeout).remain()); + instance->setConnectedCb(nullptr); + ASSERT_TRUE(status == std::future_status::timeout); +} + +TEST_F(MqttStreamingProtocolTest, WrongIdConnection) +{ + auto ok = createConnection("127.0.0.1", ""); + ASSERT_FALSE(ok); + + auto status = connectedFuture.wait_for(Timer(failureTimeout).remain()); + instance->setConnectedCb(nullptr); + ASSERT_TRUE(status == std::future_status::timeout); +} + +TEST_F(MqttStreamingProtocolTest, WrongPortConnection) +{ + auto ok = createConnection("127.0.0.1:1888", clientId); + ASSERT_TRUE(ok); + + auto status = connectedFuture.wait_for(Timer(failureTimeout).remain()); + instance->setConnectedCb(nullptr); + ASSERT_TRUE(status == std::future_status::timeout); +} + +TEST_F(MqttStreamingProtocolTest, Connected) +{ + auto ok = connect("127.0.0.1", clientId); + ASSERT_TRUE(ok); + ASSERT_TRUE(instance->isConnected() == MqttConnectionStatus::connected); +} + +TEST_F(MqttStreamingProtocolTest, Disconnection) +{ + auto ok = createConnection("127.0.0.1", clientId); + ASSERT_TRUE(ok); + + Timer timer(successTimeout); + auto status = connectedFuture.wait_for(timer.remain()); + instance->setConnectedCb(nullptr); + ASSERT_TRUE(status == std::future_status::ready); + ASSERT_TRUE(connectedFuture.get()); + ASSERT_TRUE(instance->isConnected() == MqttConnectionStatus::connected); + + // It is necessary to give the client time to disconnect. + std::atomic done{false}; + std::promise disconnectedPromise; + auto disconnectedFuture = disconnectedPromise.get_future(); + instance->setDisconnectCb([promise = &disconnectedPromise, &done](bool result) { + bool expected = false; + if (done.compare_exchange_strong(expected, true)) { + promise->set_value(result); + } + }); + + auto disconnectionOk = instance->disconnect(); + ASSERT_TRUE(disconnectionOk); + + status = disconnectedFuture.wait_for(timer.remain()); + instance->setDisconnectCb(nullptr); + ASSERT_TRUE(status == std::future_status::ready); + ASSERT_TRUE(disconnectedFuture.get()); + ASSERT_TRUE(instance->isConnected() == MqttConnectionStatus::not_connected); +} + +TEST_F(MqttStreamingProtocolTest, NotConnected) +{ + ASSERT_TRUE(instance->isConnected() == MqttConnectionStatus::not_connected); +} + +TEST_F(MqttStreamingProtocolTest, PublishingWithoutDataControl) +{ + auto ok = connect("127.0.0.1", clientId); + ASSERT_TRUE(ok); + + int token = 0; + std::atomic sendDone{false}; + std::promise sendPromise; + auto sendFuture = sendPromise.get_future(); + instance->setSentCb([promise = &sendPromise, token = &token, &sendDone](int receivedToken, bool result) { + bool expected = false; + if (receivedToken == *token) { + if (sendDone.compare_exchange_strong(expected, true)) { + promise->set_value(true); + } + } + }); + + std::atomic deliveryDone{false}; + std::promise deliveryPromise; + auto deliveryFuture = deliveryPromise.get_future(); + instance->setDeliveryCompletedCb( + [promise = &deliveryPromise, token = &token, &deliveryDone](int receivedToken) { + bool expected = false; + if (receivedToken == *token) { + if (deliveryDone.compare_exchange_strong(expected, true)) { + promise->set_value(true); + } + } + }); + + const std::string topic = buildTopicName(); + const std::string data = "test data"; + + ok = instance->publish(topic, (void *)(data.c_str()), data.size(), nullptr, 1, &token, false); + ASSERT_TRUE(ok); + ASSERT_TRUE(token != 0); + + + Timer timer(successTimeout); + auto status = sendFuture.wait_for(timer.remain()); + instance->setSentCb(nullptr); + ASSERT_TRUE(status == std::future_status::ready); + ASSERT_TRUE(sendFuture.get()); + + status = deliveryFuture.wait_for(timer.remain()); + instance->setDeliveryCompletedCb(nullptr); + ASSERT_TRUE(status == std::future_status::ready); + ASSERT_TRUE(deliveryFuture.get()); +} + +TEST_F(MqttStreamingProtocolTest, PublishingRetainedWithNullData) +{ + { + auto ok = connect("127.0.0.1", clientId); + ASSERT_TRUE(ok); + } + const std::string topic = buildTopicName(); + { + auto ok = removeRetainedTopic(topic); + ASSERT_TRUE(ok); + } +} + +TEST_F(MqttStreamingProtocolTest, PublishingRetainedWithReceivingControl) +{ + ASSERT_TRUE(connect("127.0.0.1", clientId)); + + const std::string topic = buildTopicName(); + ASSERT_TRUE(removeRetainedTopic(topic)); + + const std::string text = "test data"; + const MqttMessage msg(topic, std::vector(text.begin(), text.end()), 1, true); + + ASSERT_TRUE(publishMsg(msg)); + ASSERT_TRUE(instance->disconnect()); + + + std::this_thread::sleep_for(milliseconds(500)); // Give some time to the broker to store the retained message) + + MqttAsyncClientWrapper subscriber(std::make_shared(), "testSubscriberId"); + ASSERT_TRUE(subscriber.connect("127.0.0.1")); + + std::promise receivedPromise; + auto receivedFuture = receivedPromise.get_future(); + std::atomic done{false}; + subscriber.instance + ->setMessageArrivedCb(msg.getTopic(), + [&msg, + &done, + promise = &receivedPromise](const mqtt::MqttAsyncClient &subscriber, + mqtt::MqttMessage &receivedMsg) { + if (receivedMsg.getData().empty()) { + return; + } + bool expected = false; + if (done.compare_exchange_strong(expected, true)) { + promise->set_value(receivedMsg); + } + }); + + Timer receiveTimer(successTimeout); + auto ok = subscriber.instance->subscribe(msg.getTopic(), msg.getQos()); + ASSERT_TRUE(ok); + auto status = receivedFuture.wait_for(receiveTimer.remain()); + instance->setMessageArrivedCb(nullptr); + ASSERT_TRUE(status == std::future_status::ready); + + auto receivedMsg = receivedFuture.get(); + ASSERT_TRUE(receivedMsg == msg); + // ASSERT_TRUE(receivedMsg.getRetained()); + +} + +TEST_F(MqttStreamingProtocolTest, PublishingWithReceivingControl) +{ + ASSERT_TRUE(connect("127.0.0.1", clientId)); + + const std::string topic = buildTopicName(); + ASSERT_TRUE(removeRetainedTopic(topic)); + + const std::string text = "test data"; + const MqttMessage msg(topic, std::vector(text.begin(), text.end()), 1, false); + MqttAsyncClientWrapper subscriber(std::make_shared(), "testSubscriberId"); + ASSERT_TRUE(subscriber.connect("127.0.0.1")); + + std::promise receivedPromise; + auto receivedFuture = receivedPromise.get_future(); + std::atomic done{false}; + subscriber.instance + ->setMessageArrivedCb(msg.getTopic(), + [&msg, + &done, + promise = &receivedPromise](const mqtt::MqttAsyncClient &subscriber, + mqtt::MqttMessage &receivedMsg) { + if (receivedMsg.getData().empty()) { + return; + } + bool expected = false; + if (done.compare_exchange_strong(expected, true)) { + promise->set_value(receivedMsg); + } + }); + + Timer receiveTimer(successTimeout); + auto ok = subscriber.instance->subscribe(msg.getTopic(), msg.getQos()); + ASSERT_TRUE(ok); + ASSERT_TRUE(publishMsg(msg)); + + auto status = receivedFuture.wait_for(receiveTimer.remain()); + instance->setMessageArrivedCb(nullptr); + ASSERT_TRUE(status == std::future_status::ready); + + auto receivedMsg = receivedFuture.get(); + ASSERT_TRUE(receivedMsg == msg); +} + +TEST_F(MqttStreamingProtocolTest, PublishingWithoutConnection) +{ + const std::string topic = buildTopicName(); + const std::string data = "test data"; + int token = 0; + auto ok = instance->publish(topic, (void *)(data.c_str()), data.size(), nullptr, 1, &token, false); + ASSERT_FALSE(ok); +} diff --git a/mqtt_streaming_server_module/CMakeLists.txt b/mqtt_streaming_server_module/CMakeLists.txt index ebae7fd6..d8a37f2e 100644 --- a/mqtt_streaming_server_module/CMakeLists.txt +++ b/mqtt_streaming_server_module/CMakeLists.txt @@ -1,5 +1,13 @@ cmake_minimum_required(VERSION 3.10) -get_current_folder_name(TARGET_FOLDER_NAME) -project(MQTTModule VERSION 3.4.0 LANGUAGES CXX) +set_cmake_folder_context(TARGET_FOLDER_NAME) + +set(MQTT_SERVER_MODULE_VERSION ${OPENDAQ_PACKAGE_VERSION}) +set(MQTT_SERVER_MODULE_PRJ_NAME "OpenDaqMqttServerModule") + +message(STATUS "${MQTT_SERVER_MODULE_PRJ_NAME} version: ${MQTT_SERVER_MODULE_VERSION}") +project(${MQTT_SERVER_MODULE_PRJ_NAME} VERSION ${MQTT_SERVER_MODULE_VERSION} LANGUAGES C CXX) add_subdirectory(src) +if (OPENDAQ_MQTT_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/mqtt_streaming_server_module/include/mqtt_streaming_server_module/constants.h b/mqtt_streaming_server_module/include/mqtt_streaming_server_module/constants.h new file mode 100644 index 00000000..9cc5512b --- /dev/null +++ b/mqtt_streaming_server_module/include/mqtt_streaming_server_module/constants.h @@ -0,0 +1,31 @@ +#pragma once + +#include "common.h" + +BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_SERVER_MODULE + +static const char* MODULE_NAME = "OpenDAQMQTTServerModule"; +static const char* MODULE_ID = "OpenDAQMQTTClientModule"; +static constexpr const char* SERVER_ID_AND_CAPABILITY = "OpenDAQMQTT"; + +static constexpr const char* DEFAULT_BROKER_ADDRESS = "127.0.0.1"; +static constexpr uint16_t DEFAULT_PORT = 1883; +static constexpr const char* DEFAULT_USERNAME = ""; +static constexpr const char* DEFAULT_PASSWORD = ""; +static constexpr size_t DEFAULT_MAX_PACKET_READ_COUNT = 1000; +static constexpr uint16_t DEFAULT_POLLING_PERIOD = 20; // milliseconds + +static const char* MQTT_PREFIX = "daq.mqtt"; +static const char* CONNECTION_TYPE = "TCP/IP"; + +static constexpr const char* PROPERTY_NAME_MQTT_BROKER_ADDRESS = "MqttBrokerAddress"; +static constexpr const char* PROPERTY_NAME_MQTT_BROKER_PORT = "MqttBrokerPort"; +static constexpr const char* PROPERTY_NAME_MQTT_USERNAME = "MqttUsername"; +static constexpr const char* PROPERTY_NAME_MQTT_PASSWORD = "MqttPassword"; +static constexpr const char* PROPERTY_NAME_MAX_PACKET_READ_COUNT = "MaxPacketReadCount"; +static constexpr const char* PROPERTY_NAME_POLLING_PERIOD = "StreamingDataPollingPeriod"; + +static const char* TOPIC_ALL_SIGNALS_PREFIX = "openDAQ"; +static const char* DEVICE_SIGNAL_LIST = "$signals"; + +END_NAMESPACE_OPENDAQ_MQTT_STREAMING_SERVER_MODULE diff --git a/mqtt_streaming_server_module/include/mqtt_streaming_server_module/mqtt_streaming_server_impl.h b/mqtt_streaming_server_module/include/mqtt_streaming_server_module/mqtt_streaming_server_impl.h index adcc3647..21004054 100644 --- a/mqtt_streaming_server_module/include/mqtt_streaming_server_module/mqtt_streaming_server_impl.h +++ b/mqtt_streaming_server_module/include/mqtt_streaming_server_module/mqtt_streaming_server_impl.h @@ -22,24 +22,14 @@ #include #include #include -//#include #include #include -#include +#include +#include BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_SERVER_MODULE -namespace Mqtt::Utils::Settings { - struct MqttConnectionSettings { - std::string mqttUrl; - int port; - std::string username; - std::string password; - std::string clientId; - }; -} - struct ChannelData { std::vector data; std::vector timestamps; @@ -57,29 +47,18 @@ class MqttStreamingServerImpl : public daq::Server static PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& config, const ContextPtr& context); protected: - PropertyObjectPtr getDiscoveryConfig() override; void onStopServer() override; - StreamingPtr onGetStreaming(); void connectSignalReaders(); bool isSignalCompatible(const SignalPtr& signal); void addReader(SignalPtr signalToRead); - void removeReader(SignalPtr signalToRead); void stopServerInternal(); - void addSignalsOfComponent(ComponentPtr& component); - void componentAdded(ComponentPtr& sender, CoreEventArgsPtr& eventArgs); - void componentRemoved(ComponentPtr& sender, CoreEventArgsPtr& eventArgs); - void componentUpdated(ComponentPtr& updatedComponent); - void coreEventCallback(ComponentPtr& sender, CoreEventArgsPtr& eventArgs); - void setupMqttPublisher(); void sendData(const std::string& topic, const ChannelData& data, SizeT readAmount); - std::vector prepareJsonMessages(const std::string& topic, const ChannelData& data, SizeT dataAmount); + std::vector prepareJsonMessages(const ChannelData& data, SizeT dataAmount); std::string prepareJsonTopics(); - std::string buildTopicFromId(const std::string& globalId); - std::string buildSignalsTopic(); void sendTopicList(); void readMqttSettings(); @@ -97,13 +76,13 @@ class MqttStreamingServerImpl : public daq::Server LoggerPtr logger; LoggerComponentPtr loggerComponent; - bool serverStopped; + std::atomic serverStopped; size_t maxPacketReadCount; std::chrono::milliseconds processingThreadSleepTime; - mqtt::MqttAsyncPublisher publisher; + mqtt::MqttAsyncClient publisher; Mqtt::Utils::Settings::MqttConnectionSettings connectionSettings; std::mutex readersSync; - bool processingThreadRunning; + std::atomic processingThreadRunning; std::thread processingThread; std::atomic topicsAreSent = false; diff --git a/mqtt_streaming_server_module/src/CMakeLists.txt b/mqtt_streaming_server_module/src/CMakeLists.txt index ad1d9cd4..b78e3e59 100644 --- a/mqtt_streaming_server_module/src/CMakeLists.txt +++ b/mqtt_streaming_server_module/src/CMakeLists.txt @@ -10,6 +10,7 @@ set(SRC_Include common.h module_dll.h mqtt_streaming_server_module_impl.h mqtt_streaming_server_impl.h + constants.h ) prepend_include(mqtt_streaming_server_module SRC_Include) @@ -19,11 +20,16 @@ set(SRC_Srcs module_dll.cpp mqtt_streaming_server_impl.cpp ) +source_group("common" FILES ${MODULE_HEADERS_DIR}/common.h + ${MODULE_HEADERS_DIR}/constants.h +) + source_group("module" FILES ${MODULE_HEADERS_DIR}/mqtt_streaming_server_module_impl.h ${MODULE_HEADERS_DIR}/mqtt_streaming_server_impl.h ${MODULE_HEADERS_DIR}/module_dll.h module_dll.cpp mqtt_streaming_server_module_impl.cpp + mqtt_streaming_server_impl.cpp ) diff --git a/mqtt_streaming_server_module/src/mqtt_streaming_server_impl.cpp b/mqtt_streaming_server_module/src/mqtt_streaming_server_impl.cpp index 0125e546..b48ba788 100644 --- a/mqtt_streaming_server_module/src/mqtt_streaming_server_impl.cpp +++ b/mqtt_streaming_server_module/src/mqtt_streaming_server_impl.cpp @@ -1,74 +1,53 @@ -#include -#include -#include #include -#include -#include -#include -#include +#include +#include +#include #include -#include #include #include +#include +#include +#include +#include +#include -#include #include #include - +#include +#include #include -#include "rapidjson/writer.h" -#include "rapidjson/stringbuffer.h" BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_SERVER_MODULE using namespace daq; -static constexpr size_t DEFAULT_MAX_PACKET_READ_COUNT = 1000; -static constexpr uint16_t DEFAULT_POLLING_PERIOD = 20; -static constexpr uint16_t DEFAULT_PORT = 1883; -static constexpr const char* DEFAULT_ADDRESS = "127.0.0.1"; -static constexpr const char* DEFAULT_USERNAME = ""; -static constexpr const char* DEFAULT_PASSWORD = ""; -static constexpr const char* SERVER_ID_AND_CAPABILITY = "OpenDAQMQTT"; - -static constexpr const char* PROPERTY_NAME_MQTT_BROKER_URL = "BrokerAddress"; -static constexpr const char* PROPERTY_NAME_MQTT_BROKER_PORT = "MqttBrokerPort"; -static constexpr const char* PROPERTY_NAME_MQTT_USERNAME = "MqttUsername"; -static constexpr const char* PROPERTY_NAME_MQTT_PASSWORD = "MqttPassword"; - -MqttStreamingServerImpl::MqttStreamingServerImpl(const DevicePtr& rootDevice, - const PropertyObjectPtr& config, - const ContextPtr& context) - : Server(SERVER_ID_AND_CAPABILITY, config, rootDevice, context) - , signals(List()) - , rootDeviceGlobalId(rootDevice.getGlobalId().toStdString()) - , logger(context.getLogger()) - , loggerComponent(logger.getOrAddComponent(id)) - , serverStopped(false) - , publisher() - , connectionSettings({ "", DEFAULT_PORT, "", "", rootDevice.getGlobalId().toStdString()}) +MqttStreamingServerImpl::MqttStreamingServerImpl(const DevicePtr& rootDevice, const PropertyObjectPtr& config, const ContextPtr& context) + : Server(SERVER_ID_AND_CAPABILITY, config, rootDevice, context), + signals(List()), + rootDeviceGlobalId(rootDevice.getGlobalId().toStdString()), + logger(context.getLogger()), + loggerComponent(logger.getOrAddComponent(id)), + serverStopped(false), + publisher(), + connectionSettings({DEFAULT_BROKER_ADDRESS, DEFAULT_PORT, DEFAULT_USERNAME, DEFAULT_PASSWORD, rootDevice.getGlobalId().toStdString()}) { auto info = rootDevice.getInfo(); if (info.hasServerCapability(SERVER_ID_AND_CAPABILITY)) - DAQ_THROW_EXCEPTION(InvalidStateException, fmt::format("Device \"{}\" already has an {} server capability.", info.getName(), SERVER_ID_AND_CAPABILITY)); + DAQ_THROW_EXCEPTION(InvalidStateException, + fmt::format("Device \"{}\" already has an {} server capability.", info.getName(), SERVER_ID_AND_CAPABILITY)); readMqttSettings(); - StringPtr path = config.getPropertyValue("Path"); - ServerCapabilityConfigPtr serverCapabilityStreaming = ServerCapability(SERVER_ID_AND_CAPABILITY, SERVER_ID_AND_CAPABILITY, ProtocolType::Streaming) - .setPrefix("daq.mqtts") - .setConnectionType("TCP/IP") - .setPort(connectionSettings.port); + .setPrefix(MQTT_PREFIX) + .setConnectionType(CONNECTION_TYPE) + .setPort(connectionSettings.port); - serverCapabilityStreaming.addProperty(StringProperty("Path", path == "/" ? "" : path)); info.asPtr(true).addServerCapability(serverCapabilityStreaming); - this->context.getOnCoreEvent() += event(&MqttStreamingServerImpl::coreEventCallback); - - maxPacketReadCount = config.getPropertyValue("MaxPacketReadCount"); - processingThreadSleepTime = std::chrono::milliseconds(config.getPropertyValue("StreamingDataPollingPeriod")); + maxPacketReadCount = config.getPropertyValue(PROPERTY_NAME_MAX_PACKET_READ_COUNT); + processingThreadSleepTime = std::chrono::milliseconds(config.getPropertyValue(PROPERTY_NAME_POLLING_PERIOD)); buffer.data.resize(maxPacketReadCount); buffer.timestamps.resize(maxPacketReadCount); @@ -83,92 +62,13 @@ MqttStreamingServerImpl::~MqttStreamingServerImpl() stopServerInternal(); } -void MqttStreamingServerImpl::addSignalsOfComponent(ComponentPtr& component) -{ - if (component.supportsInterface()) - { - LOG_I("Added Signal: {};", component.getGlobalId()); - //serverHandler->addSignal(component.asPtr(true)); - } - else if (component.supportsInterface()) - { - auto nestedComponents = component.asPtr().getItems(search::Recursive(search::Any())); - for (const auto& nestedComponent : nestedComponents) - { - if (nestedComponent.supportsInterface()) - { - LOG_I("Added Signal: {};", nestedComponent.getGlobalId()); - //serverHandler->addSignal(nestedComponent.asPtr(true)); - } - } - } -} - -void MqttStreamingServerImpl::componentAdded(ComponentPtr& /*sender*/, CoreEventArgsPtr& eventArgs) -{ - ComponentPtr addedComponent = eventArgs.getParameters().get("Component"); - - auto addedComponentGlobalId = addedComponent.getGlobalId().toStdString(); - if (addedComponentGlobalId.find(rootDeviceGlobalId) != 0) - return; - - LOG_I("Added Component: {};", addedComponentGlobalId); - addSignalsOfComponent(addedComponent); -} - -void MqttStreamingServerImpl::componentRemoved(ComponentPtr& sender, CoreEventArgsPtr& eventArgs) -{ - StringPtr removedComponentLocalId = eventArgs.getParameters().get("Id"); - - auto removedComponentGlobalId = - sender.getGlobalId().toStdString() + "/" + removedComponentLocalId.toStdString(); - if (removedComponentGlobalId.find(rootDeviceGlobalId) != 0) - return; - - LOG_I("Component: {}; is removed", removedComponentGlobalId); - //serverHandler->removeComponentSignals(removedComponentGlobalId); -} - -void MqttStreamingServerImpl::componentUpdated(ComponentPtr& updatedComponent) -{ - auto updatedComponentGlobalId = updatedComponent.getGlobalId().toStdString(); - if (updatedComponentGlobalId.find(rootDeviceGlobalId) != 0) - return; - - LOG_I("Component: {}; is updated", updatedComponentGlobalId); - - // remove all registered signal of updated component since those might be modified or removed - //serverHandler->removeComponentSignals(updatedComponentGlobalId); - - // add updated versions of signals - addSignalsOfComponent(updatedComponent); -} - -void MqttStreamingServerImpl::coreEventCallback(ComponentPtr& sender, CoreEventArgsPtr& eventArgs) -{ - switch (static_cast(eventArgs.getEventId())) - { - case CoreEventId::ComponentAdded: - componentAdded(sender, eventArgs); - break; - case CoreEventId::ComponentRemoved: - componentRemoved(sender, eventArgs); - break; - case CoreEventId::ComponentUpdateEnd: - componentUpdated(sender); - break; - default: - break; - } -} - void MqttStreamingServerImpl::setupMqttPublisher() { publisher.disconnect(); publisher.setServerURL(connectionSettings.mqttUrl); publisher.setClientId(connectionSettings.clientId); publisher.setUsernamePasswrod(connectionSettings.username, connectionSettings.password); - publisher.setOnConnect([this]() { LOG_I("MQTT: Connection established"); }); + publisher.setConnectedCb([this]() { LOG_I("MQTT: Connection established"); }); LOG_I("MQTT: Trying to connect to MQTT broker ({})", connectionSettings.mqttUrl); publisher.connect(); @@ -179,34 +79,28 @@ void MqttStreamingServerImpl::sendData(const std::string& topic, const ChannelDa if (readAmount == 0) return; - const auto jsonMessages = prepareJsonMessages(topic, data, readAmount); - if (publisher.isConnected() == mqtt::MqttConnectionStatus::connected) { - for (const auto& jsonMessage : jsonMessages) { + const auto jsonMessages = prepareJsonMessages(data, readAmount); + if (publisher.isConnected() == mqtt::MqttConnectionStatus::connected) + { + for (const auto& jsonMessage : jsonMessages) + { std::string err; - auto status = publisher.publish(topic, (void*) jsonMessage.c_str(), jsonMessage.length(), &err); - if (!status) { + auto status = publisher.publish(topic, (void*)jsonMessage.c_str(), jsonMessage.length(), &err); + if (!status) + { LOG_W("Failed to publish data to {}; reason - {}", topic, err); } } } } -std::vector MqttStreamingServerImpl::prepareJsonMessages(const std::string& topic, const ChannelData& data, SizeT dataAmount) +std::vector MqttStreamingServerImpl::prepareJsonMessages(const ChannelData& data, SizeT dataAmount) { std::vector result; - for (size_t i = 0; i < dataAmount; ++i) { - rapidjson::Document doc; - doc.SetObject(); - doc.AddMember("value", rapidjson::Value(data.data[i]), doc.GetAllocator()); - doc.AddMember("timestamp", rapidjson::Value(data.timestamps[i]), doc.GetAllocator()); - - // Serialize to string - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - doc.Accept(writer); - - result.emplace_back(buffer.GetString()); + for (size_t i = 0; i < dataAmount; ++i) + { + result.emplace_back(mqtt::MqttDataWrapper::serializeSampleData({data.data[i], data.timestamps[i]})); } return result; @@ -214,45 +108,22 @@ std::vector MqttStreamingServerImpl::prepareJsonMessages(const std: std::string MqttStreamingServerImpl::prepareJsonTopics() { - std::string result; - rapidjson::Document doc; - doc.SetArray(); - for (const auto& signal : signals) { - rapidjson::Value topicValue; - topicValue.SetString(buildTopicFromId(signal.getGlobalId().toStdString()).c_str(), doc.GetAllocator()); - doc.PushBack(topicValue, doc.GetAllocator()); - } - - // Serialize to string - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - doc.Accept(writer); - - result = buffer.GetString(); - - - return result; -} - -std::string MqttStreamingServerImpl::buildTopicFromId(const std::string& globalId) -{ - return ("openDAQ" + globalId); -} - -std::string MqttStreamingServerImpl::buildSignalsTopic() -{ - return ("openDAQ" + rootDeviceGlobalId + "/$signals"); + return mqtt::MqttDataWrapper::serializeSignalDescriptors(signals); } void MqttStreamingServerImpl::sendTopicList() { - std::string topic = buildSignalsTopic(); + std::string topic = mqtt::MqttDataWrapper::buildSignalsTopic(rootDeviceGlobalId); auto topicsMessage = prepareJsonTopics(); - if (publisher.isConnected() == mqtt::MqttConnectionStatus::connected) { - bool status = publisher.publish(topic, (void*) topicsMessage.c_str(), topicsMessage.length(), nullptr, 1, nullptr, true); - if (!status) { + if (publisher.isConnected() == mqtt::MqttConnectionStatus::connected) + { + bool status = publisher.publish(topic, (void*)topicsMessage.c_str(), topicsMessage.length(), nullptr, 1, nullptr, true); + if (!status) + { LOG_W("Failed to publish topics list to {}", topic); - } else { + } + else + { topicsAreSent = true; } } @@ -260,7 +131,7 @@ void MqttStreamingServerImpl::sendTopicList() void MqttStreamingServerImpl::readMqttSettings() { - connectionSettings.mqttUrl = (std::string)config.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_URL); + connectionSettings.mqttUrl = (std::string)config.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_ADDRESS); connectionSettings.port = config.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_PORT); connectionSettings.username = (std::string)config.getPropertyValue(PROPERTY_NAME_MQTT_USERNAME); connectionSettings.password = (std::string)config.getPropertyValue(PROPERTY_NAME_MQTT_PASSWORD); @@ -269,8 +140,8 @@ void MqttStreamingServerImpl::readMqttSettings() void MqttStreamingServerImpl::processingThreadFunc() { - daqNameThread("MqttC2DSread"); - LOG_I("Streaming-to-device read thread started") + daqNameThread("MqttStrmSrvRead"); + LOG_I("Streaming read thread started") while (processingThreadRunning) { { @@ -288,25 +159,23 @@ void MqttStreamingServerImpl::processingThreadFunc() continue; daq::SizeT readAmount = maxPacketReadCount; reader.readWithDomain(buffer.data.data(), buffer.timestamps.data(), &readAmount); - sendData(buildTopicFromId(signals[i].getGlobalId().toStdString()), buffer, readAmount); - + sendData(mqtt::MqttDataWrapper::buildTopicFromId(signals[i].getGlobalId().toStdString()), buffer, readAmount); if (reader.getAvailableCount() > 0) hasPacketsToRead = true; } - } - while(hasPacketsToRead); + } while (hasPacketsToRead); } std::this_thread::sleep_for(processingThreadSleepTime); } - LOG_I("Streaming-to-device read thread stopped"); + LOG_I("Streaming read thread stopped"); } void MqttStreamingServerImpl::startProcessingThread() { - assert(!processingThreadRunning); - processingThreadRunning = true; + if (processingThreadRunning.exchange(true)) + return; processingThread = std::thread(&MqttStreamingServerImpl::processingThreadFunc, this); } @@ -322,12 +191,10 @@ void MqttStreamingServerImpl::stopProcessingThread() void MqttStreamingServerImpl::stopServerInternal() { - if (serverStopped) - return; - serverStopped = true; + if (serverStopped.exchange(true)) + return; - this->context.getOnCoreEvent() -= event(&MqttStreamingServerImpl::coreEventCallback); if (const DevicePtr rootDevice = this->rootDeviceRef.assigned() ? this->rootDeviceRef.getRef() : nullptr; rootDevice.assigned() && !rootDevice.isRemoved()) { @@ -357,11 +224,13 @@ void MqttStreamingServerImpl::connectSignalReaders() bool MqttStreamingServerImpl::isSignalCompatible(const SignalPtr& signal) { - if (!signal.getDomainSignal().assigned()) { + if (!signal.getDomainSignal().assigned()) + { LOG_I("Signal {} doesn't has domain signal assigned, skipping", signal.getGlobalId().toStdString()); return false; } - if (!signal.getDescriptor().assigned()) { + if (!signal.getDescriptor().assigned()) + { LOG_I("Signal {} doesn't has descriptor assigned, skipping", signal.getGlobalId().toStdString()); return false; } @@ -371,23 +240,16 @@ bool MqttStreamingServerImpl::isSignalCompatible(const SignalPtr& signal) return false; } if (const auto sampleType = signal.getDescriptor().getSampleType(); - sampleType != SampleType::Float64 && - sampleType != SampleType::Float32 && - sampleType != SampleType::Int8 && - sampleType != SampleType::Int16 && - sampleType != SampleType::Int32 && - sampleType != SampleType::Int64 && - sampleType != SampleType::UInt8 && - sampleType != SampleType::UInt16 && - sampleType != SampleType::UInt32 && + sampleType != SampleType::Float64 && sampleType != SampleType::Float32 && sampleType != SampleType::Int8 && + sampleType != SampleType::Int16 && sampleType != SampleType::Int32 && sampleType != SampleType::Int64 && + sampleType != SampleType::UInt8 && sampleType != SampleType::UInt16 && sampleType != SampleType::UInt32 && sampleType != SampleType::UInt64) { LOG_I("Signal {} has uncompatible sample type, skipping", signal.getGlobalId().toStdString()); return false; } if (const auto domainSampleType = signal.getDomainSignal().getDescriptor().getSampleType(); - domainSampleType != SampleType::Int64 && - domainSampleType != SampleType::UInt64) + domainSampleType != SampleType::Int64 && domainSampleType != SampleType::UInt64) { LOG_I("Signal {} has uncompatible domain signal sample type, skipping", signal.getGlobalId().toStdString()); return false; @@ -414,10 +276,10 @@ void MqttStreamingServerImpl::populateDefaultConfigFromProvider(const ContextPtr PropertyObjectPtr MqttStreamingServerImpl::createDefaultConfig(const ContextPtr& context) { - //auto defaultConfig = MqttStreamingServerHandler::createDefaultConfig(); + // auto defaultConfig = MqttStreamingServerHandler::createDefaultConfig(); auto defaultConfig = PropertyObject(); - const auto pollingPeriodProp = IntPropertyBuilder("StreamingDataPollingPeriod", DEFAULT_POLLING_PERIOD) + const auto pollingPeriodProp = IntPropertyBuilder(PROPERTY_NAME_POLLING_PERIOD, DEFAULT_POLLING_PERIOD) .setMinValue(1) .setMaxValue(65535) .setDescription("Polling period in milliseconds " @@ -426,43 +288,32 @@ PropertyObjectPtr MqttStreamingServerImpl::createDefaultConfig(const ContextPtr& .build(); defaultConfig.addProperty(pollingPeriodProp); - const auto maxPacketReadCountProp = IntPropertyBuilder("MaxPacketReadCount", DEFAULT_MAX_PACKET_READ_COUNT) - .setMinValue(1) - .setDescription("Specifies the size of a pre-allocated packet buffer into " - "which packets are dequeued. The size determines the amount of " - "packets that can be read in one dequeue call. Should be greater " - "than the amount of packets generated per polling period for best " - "performance.") - .build(); + const auto maxPacketReadCountProp = IntPropertyBuilder(PROPERTY_NAME_MAX_PACKET_READ_COUNT, DEFAULT_MAX_PACKET_READ_COUNT) + .setMinValue(1) + .setDescription("Specifies the size of a pre-allocated packet buffer into " + "which packets are dequeued. The size determines the amount of " + "packets that can be read in one dequeue call. Should be greater " + "than the amount of packets generated per polling period for best " + "performance.") + .build(); defaultConfig.addProperty(maxPacketReadCountProp); - const auto url = StringPropertyBuilder(PROPERTY_NAME_MQTT_BROKER_URL, DEFAULT_ADDRESS) - .setDescription("") - .build(); + const auto url = StringPropertyBuilder(PROPERTY_NAME_MQTT_BROKER_ADDRESS, DEFAULT_BROKER_ADDRESS).setDescription("").build(); defaultConfig.addProperty(url); const auto port = IntPropertyBuilder(PROPERTY_NAME_MQTT_BROKER_PORT, DEFAULT_PORT) - .setMinValue(1) - .setMaxValue(65535) - .setDescription("Port is not used") - .build(); + .setMinValue(1) + .setMaxValue(65535) + .setDescription("Port is not used") + .build(); defaultConfig.addProperty(port); - const auto username = StringPropertyBuilder(PROPERTY_NAME_MQTT_USERNAME, DEFAULT_USERNAME) - .setDescription("") - .build(); + const auto username = StringPropertyBuilder(PROPERTY_NAME_MQTT_USERNAME, DEFAULT_USERNAME).setDescription("").build(); defaultConfig.addProperty(username); - const auto password = StringPropertyBuilder(PROPERTY_NAME_MQTT_PASSWORD, DEFAULT_PASSWORD) - .setDescription("") - .build(); + const auto password = StringPropertyBuilder(PROPERTY_NAME_MQTT_PASSWORD, DEFAULT_PASSWORD).setDescription("").build(); defaultConfig.addProperty(password); - const auto path = StringPropertyBuilder("Path", "") - .setDescription("") - .build(); - defaultConfig.addProperty(path); - populateDefaultConfigFromProvider(context, defaultConfig); return defaultConfig; } @@ -480,24 +331,12 @@ PropertyObjectPtr MqttStreamingServerImpl::populateDefaultConfig(const PropertyO return defConfig; } -PropertyObjectPtr MqttStreamingServerImpl::getDiscoveryConfig() -{ - auto discoveryConfig = PropertyObject(); - discoveryConfig.addProperty(StringProperty("ServiceName", "_opendaq-streaming-mqtt._tcp.local.")); - discoveryConfig.addProperty(StringProperty("ServiceCap", "OPENDAQ_MQTTS")); - discoveryConfig.addProperty(StringProperty("Path", config.getPropertyValue("Path"))); - discoveryConfig.addProperty(IntProperty("Port", 0)); - discoveryConfig.addProperty(StringProperty("ProtocolVersion", std::to_string(/*GetLatestConfigProtocolVersion()*/0))); - return discoveryConfig; -} - ServerTypePtr MqttStreamingServerImpl::createType(const ContextPtr& context) { - return ServerType( - SERVER_ID_AND_CAPABILITY, - "openDAQ MQTT Streaming server", - "Streams data over MQTT", - MqttStreamingServerImpl::createDefaultConfig(context)); + return ServerType(SERVER_ID_AND_CAPABILITY, + "openDAQ MQTT Streaming server", + "Streams data over MQTT", + MqttStreamingServerImpl::createDefaultConfig(context)); } void MqttStreamingServerImpl::onStopServer() @@ -505,34 +344,19 @@ void MqttStreamingServerImpl::onStopServer() stopServerInternal(); } -StreamingPtr MqttStreamingServerImpl::onGetStreaming() -{ - return nullptr; -} - void MqttStreamingServerImpl::addReader(SignalPtr signalToRead) { std::scoped_lock lock(readersSync); signals.pushBack(signalToRead); streamReaders.emplace_back(StreamReaderBuilder() - .setSignal(signalToRead) - .setInputPortNotificationMethod(PacketReadyNotification::None) - .setValueReadType(SampleType::Float64) - .setDomainReadType(SampleType::Int64) - .setSkipEvents(true) - .build()); -} - -void MqttStreamingServerImpl::removeReader(SignalPtr signalToRead) -{ - + .setSignal(signalToRead) + .setValueReadType(SampleType::Float64) + .setDomainReadType(SampleType::Int64) + .setSkipEvents(true) + .build()); } OPENDAQ_DEFINE_CLASS_FACTORY_WITH_INTERFACE( - INTERNAL_FACTORY, MqttStreamingServer, daq::IServer, - daq::DevicePtr, rootDevice, - PropertyObjectPtr, config, - const ContextPtr&, context -) + INTERNAL_FACTORY, MqttStreamingServer, daq::IServer, daq::DevicePtr, rootDevice, PropertyObjectPtr, config, const ContextPtr&, context) END_NAMESPACE_OPENDAQ_MQTT_STREAMING_SERVER_MODULE diff --git a/mqtt_streaming_server_module/src/mqtt_streaming_server_module_impl.cpp b/mqtt_streaming_server_module/src/mqtt_streaming_server_module_impl.cpp index 3506e793..025cc9e4 100644 --- a/mqtt_streaming_server_module/src/mqtt_streaming_server_module_impl.cpp +++ b/mqtt_streaming_server_module/src/mqtt_streaming_server_module_impl.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -8,12 +9,12 @@ BEGIN_NAMESPACE_OPENDAQ_MQTT_STREAMING_SERVER_MODULE MqttStreamingServerModule::MqttStreamingServerModule(ContextPtr context) - : Module("OpenDAQMqttStreamingServerModule", + : Module(MODULE_NAME, daq::VersionInfo(MQTT_STREAM_SRV_MODULE_MAJOR_VERSION, MQTT_STREAM_SRV_MODULE_MINOR_VERSION, MQTT_STREAM_SRV_MODULE_PATCH_VERSION), std::move(context), - "OpenDAQMqttStreamingServerModule") + MODULE_ID) { } diff --git a/mqtt_streaming_server_module/tests/CMakeLists.txt b/mqtt_streaming_server_module/tests/CMakeLists.txt new file mode 100644 index 00000000..9e7d19e2 --- /dev/null +++ b/mqtt_streaming_server_module/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +set(MODULE_NAME mqtt_stream_srv_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_mqtt_streaming_server_module.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + ${MODULE_NAME} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (MSVC) # Ignoring warning for the Taskflow + target_compile_options(${TEST_APP} PRIVATE /wd4324) +endif() diff --git a/mqtt_streaming_server_module/tests/test_app.cpp b/mqtt_streaming_server_module/tests/test_app.cpp new file mode 100644 index 00000000..64dd0cc6 --- /dev/null +++ b/mqtt_streaming_server_module/tests/test_app.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include +#include + +int main(int argc, char** args) +{ + daq::daqInitializeCoreObjectsTesting(); + daqInitModuleManagerLibrary(); + daqInitOpenDaqLibrary(); + + 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/mqtt_streaming_server_module/tests/test_mqtt_streaming_server_module.cpp b/mqtt_streaming_server_module/tests/test_mqtt_streaming_server_module.cpp new file mode 100644 index 00000000..b63c2e03 --- /dev/null +++ b/mqtt_streaming_server_module/tests/test_mqtt_streaming_server_module.cpp @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using MqttStreamingServerModuleTest = testing::Test; +using namespace daq; +using namespace daq::modules::mqtt_streaming_server_module; + +static ModulePtr CreateModule(ContextPtr context = NullContext(), ModuleManagerPtr manager = nullptr) +{ + ModulePtr module; + createMqttStreamingServerModule(&module, context); + return module; +} + +static InstancePtr CreateTestInstance() +{ + const auto logger = Logger(); + const auto moduleManager = ModuleManager("[[none]]"); + const auto authenticationProvider = AuthenticationProvider(); + const auto context = Context(Scheduler(logger), logger, TypeManager(), moduleManager, authenticationProvider); + + const ModulePtr daqMqttStreamingServerModule = CreateModule(context, moduleManager); + moduleManager.addModule(daqMqttStreamingServerModule); + + auto instance = InstanceCustom(context, "localInstance"); + for (const auto& deviceInfo : instance.getAvailableDevices()) + instance.addDevice(deviceInfo.getConnectionString()); + + for (const auto& [id, _] : instance.getAvailableFunctionBlockTypes()) + instance.addFunctionBlock(id); + + return instance; +} + +static PropertyObjectPtr CreateServerConfig(const InstancePtr& instance) +{ + auto config = instance.getAvailableServerTypes().get(SERVER_ID_AND_CAPABILITY).createDefaultConfig(); + return config; +} + +TEST_F(MqttStreamingServerModuleTest, CreateModule) +{ + IModule* module = nullptr; + ErrCode errCode = createModule(&module, NullContext()); + ASSERT_TRUE(OPENDAQ_SUCCEEDED(errCode)); + + ASSERT_NE(module, nullptr); + module->releaseRef(); +} + +TEST_F(MqttStreamingServerModuleTest, ModuleName) +{ + auto module = CreateModule(); + ASSERT_EQ(module.getModuleInfo().getName(), MODULE_NAME); +} + +TEST_F(MqttStreamingServerModuleTest, VersionAvailable) +{ + auto module = CreateModule(); + ASSERT_TRUE(module.getModuleInfo().getVersionInfo().assigned()); +} + +TEST_F(MqttStreamingServerModuleTest, VersionCorrect) +{ + auto module = CreateModule(); + auto version = module.getModuleInfo().getVersionInfo(); + + ASSERT_EQ(version.getMajor(), MQTT_STREAM_SRV_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), MQTT_STREAM_SRV_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), MQTT_STREAM_SRV_MODULE_PATCH_VERSION); +} + +TEST_F(MqttStreamingServerModuleTest, GetAvailableComponentTypes) +{ + const auto module = CreateModule(); + + DictPtr functionBlockTypes; + ASSERT_NO_THROW(functionBlockTypes = module.getAvailableFunctionBlockTypes()); + ASSERT_EQ(functionBlockTypes.getCount(), 0u); + + DictPtr deviceTypes; + ASSERT_NO_THROW(deviceTypes = module.getAvailableDeviceTypes()); + ASSERT_EQ(deviceTypes.getCount(), 0u); + + DictPtr serverTypes; + ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); + ASSERT_EQ(serverTypes.getCount(), 1u); + ASSERT_TRUE(serverTypes.hasKey(SERVER_ID_AND_CAPABILITY)); + ASSERT_EQ(serverTypes.get(SERVER_ID_AND_CAPABILITY).getId(), SERVER_ID_AND_CAPABILITY); + + // Check module info for module + ModuleInfoPtr moduleInfo; + ASSERT_NO_THROW(moduleInfo = module.getModuleInfo()); + ASSERT_NE(moduleInfo, nullptr); + ASSERT_EQ(moduleInfo.getName(), MODULE_NAME); + ASSERT_EQ(moduleInfo.getId(), MODULE_ID); + + // Check version info for module + VersionInfoPtr versionInfoModule; + ASSERT_NO_THROW(versionInfoModule = moduleInfo.getVersionInfo()); + ASSERT_NE(versionInfoModule, nullptr); + ASSERT_EQ(versionInfoModule.getMajor(), MQTT_STREAM_SRV_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoModule.getMinor(), MQTT_STREAM_SRV_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoModule.getPatch(), MQTT_STREAM_SRV_MODULE_PATCH_VERSION); + + // Check module version info for server types + for (const auto& serverType : serverTypes) + { + ModuleInfoPtr moduleInfoServerType; + ASSERT_NO_THROW(moduleInfoServerType = serverType.second.getModuleInfo()); + ASSERT_NE(moduleInfoServerType, nullptr); + ASSERT_EQ(moduleInfoServerType.getName(), MODULE_NAME); + ASSERT_EQ(moduleInfoServerType.getId(), MODULE_ID); + + VersionInfoPtr versionInfoServerType; + ASSERT_NO_THROW(versionInfoServerType = moduleInfoServerType.getVersionInfo()); + ASSERT_NE(versionInfoServerType, nullptr); + ASSERT_EQ(versionInfoServerType.getMajor(), MQTT_STREAM_SRV_MODULE_MAJOR_VERSION); + ASSERT_EQ(versionInfoServerType.getMinor(), MQTT_STREAM_SRV_MODULE_MINOR_VERSION); + ASSERT_EQ(versionInfoServerType.getPatch(), MQTT_STREAM_SRV_MODULE_PATCH_VERSION); + } +} + +TEST_F(MqttStreamingServerModuleTest, ServerConfig) +{ + auto module = CreateModule(); + + DictPtr serverTypes = module.getAvailableServerTypes(); + ASSERT_TRUE(serverTypes.hasKey(SERVER_ID_AND_CAPABILITY)); + auto config = serverTypes.get(SERVER_ID_AND_CAPABILITY).createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + + ASSERT_TRUE(config.hasProperty(PROPERTY_NAME_MQTT_BROKER_ADDRESS)); + ASSERT_EQ(config.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_ADDRESS), DEFAULT_BROKER_ADDRESS); + + ASSERT_TRUE(config.hasProperty(PROPERTY_NAME_MQTT_BROKER_PORT)); + ASSERT_EQ(config.getPropertyValue(PROPERTY_NAME_MQTT_BROKER_PORT), DEFAULT_PORT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_NAME_MQTT_USERNAME)); + ASSERT_EQ(config.getPropertyValue(PROPERTY_NAME_MQTT_USERNAME), DEFAULT_USERNAME); + + ASSERT_TRUE(config.hasProperty(PROPERTY_NAME_MQTT_PASSWORD)); + ASSERT_EQ(config.getPropertyValue(PROPERTY_NAME_MQTT_PASSWORD), DEFAULT_PASSWORD); + + ASSERT_TRUE(config.hasProperty(PROPERTY_NAME_MAX_PACKET_READ_COUNT)); + ASSERT_EQ(config.getPropertyValue(PROPERTY_NAME_MAX_PACKET_READ_COUNT), DEFAULT_MAX_PACKET_READ_COUNT); + + ASSERT_TRUE(config.hasProperty(PROPERTY_NAME_POLLING_PERIOD)); + ASSERT_EQ(config.getPropertyValue(PROPERTY_NAME_POLLING_PERIOD), DEFAULT_POLLING_PERIOD); +} + +TEST_F(MqttStreamingServerModuleTest, CreateServer) +{ + auto device = CreateTestInstance(); + auto module = CreateModule(device.getContext()); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(module.createServer(SERVER_ID_AND_CAPABILITY, device.getRootDevice(), config)); +} + +TEST_F(MqttStreamingServerModuleTest, CreateServerFromInstance) +{ + auto device = CreateTestInstance(); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(device.addServer(SERVER_ID_AND_CAPABILITY, config)); +}