diff --git a/external/streaming_protocol/CMakeLists.txt b/external/streaming_protocol/CMakeLists.txt new file mode 100644 index 0000000..be75a99 --- /dev/null +++ b/external/streaming_protocol/CMakeLists.txt @@ -0,0 +1,13 @@ +set(STREAMING_PROTOCOL_ALWAYS_FETCH_DEPS ON CACHE BOOL "" FORCE) + +if (OPENDAQ_ENABLE_WS_SIGGEN_INTEGRATION_TESTS) + set(STREAMING_PROTOCOL_TOOLS ON CACHE BOOL "" FORCE) +endif() + +opendaq_dependency( + NAME streaming_protocol + REQUIRED_VERSION 1.2.7 + GIT_REPOSITORY https://github.com/openDAQ/streaming-protocol-lt.git + GIT_REF v1.2.7 + EXPECT_TARGET daq::streaming_protocol +) diff --git a/modules/websocket_streaming_client_module/CMakeLists.txt b/modules/websocket_streaming_client_module/CMakeLists.txt new file mode 100644 index 0000000..be07a8f --- /dev/null +++ b/modules/websocket_streaming_client_module/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(WebsocketStreamingClientModule VERSION ${OPENDAQ_PACKAGE_VERSION} LANGUAGES C CXX) + +if (MSVC) + # loss of data / precision, unsigned <--> signed + # + # 'argument' : conversion from 'type1' to 'type2', possible loss of data + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4244 + add_compile_options(/wd4244) + + # 'var' : conversion from 'size_t' to 'type', possible loss of data + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4267 + add_compile_options(/wd4267) +endif() + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/common.h b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/common.h new file mode 100644 index 0000000..53b1e95 --- /dev/null +++ b/modules/websocket_streaming_client_module/include/websocket_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_WEBSOCKET_STREAMING_CLIENT_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(websocket_streaming_client_module) +#define END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/module_dll.h b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/module_dll.h new file mode 100644 index 0000000..925856d --- /dev/null +++ b/modules/websocket_streaming_client_module/include/websocket_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(WebsocketStreamingClientModule) diff --git a/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/websocket_streaming_client_module_impl.h b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/websocket_streaming_client_module_impl.h new file mode 100644 index 0000000..b46b810 --- /dev/null +++ b/modules/websocket_streaming_client_module/include/websocket_streaming_client_module/websocket_streaming_client_module_impl.h @@ -0,0 +1,55 @@ +/* + * 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_WEBSOCKET_STREAMING_CLIENT_MODULE + +class WebsocketStreamingClientModule final : public Module +{ +public: + WebsocketStreamingClientModule(ContextPtr context); + + ListPtr onGetAvailableDevices() override; + DictPtr onGetAvailableDeviceTypes() override; + DictPtr onGetAvailableStreamingTypes() override; + DevicePtr onCreateDevice(const StringPtr& connectionString, + const ComponentPtr& parent, + const PropertyObjectPtr& config) override; + bool acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); + bool acceptsStreamingConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config); + StreamingPtr onCreateStreaming(const StringPtr& connectionString, const PropertyObjectPtr& config) override; + Bool onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) override; + +private: + static DeviceTypePtr createWebsocketDeviceType(bool useOldPrefix); + static StringPtr createUrlConnectionString(const StringPtr& host, + const IntegerPtr& port, + const StringPtr& path); + static StreamingTypePtr createWebsocketStreamingType(); + static PropertyObjectPtr createDefaultConfig(); + static StringPtr formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config); + static DeviceInfoPtr populateDiscoveredDevice(const discovery::MdnsDiscoveredDevice& discoveredDevice); + + std::mutex sync; + size_t deviceIndex; + discovery::DiscoveryClient discoveryClient; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE diff --git a/modules/websocket_streaming_client_module/src/CMakeLists.txt b/modules/websocket_streaming_client_module/src/CMakeLists.txt new file mode 100644 index 0000000..f6f495c --- /dev/null +++ b/modules/websocket_streaming_client_module/src/CMakeLists.txt @@ -0,0 +1,47 @@ +# Windows NSIS package manager limits lenght of variables +# "openDAQ_[MODULE_NAME]_[SUFFIX]" with 60 characters max +# The suffix with max lenght is [Development_was_installed] +# so the shorter module name [ws_stream_cl_module] should be used instead of full name: +# [websocket_streaming_client_module] +set(LIB_NAME ws_stream_cl_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + module_dll.h + websocket_streaming_client_module_impl.h +) + +set(SRC_Srcs module_dll.cpp + websocket_streaming_client_module_impl.cpp +) + +prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/websocket_streaming_client_module_impl.h + ${MODULE_HEADERS_DIR}/module_dll.h + module_dll.cpp + websocket_streaming_client_module_impl.cpp +) + + +add_library(${LIB_NAME} SHARED ${SRC_Include} + ${SRC_Srcs} +) +add_library(${SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +endif() + +target_link_libraries(${LIB_NAME} PUBLIC daq::opendaq + PRIVATE daq::discovery + daq::websocket_streaming +) + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + $ +) + +opendaq_set_module_properties(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) +create_version_header(${LIB_NAME}) diff --git a/modules/websocket_streaming_client_module/src/module_dll.cpp b/modules/websocket_streaming_client_module/src/module_dll.cpp new file mode 100644 index 0000000..26f176a --- /dev/null +++ b/modules/websocket_streaming_client_module/src/module_dll.cpp @@ -0,0 +1,8 @@ +#include +#include + +#include + +using namespace daq::modules::websocket_streaming_client_module; + +DEFINE_MODULE_EXPORTS(WebsocketStreamingClientModule) diff --git a/modules/websocket_streaming_client_module/src/websocket_streaming_client_module_impl.cpp b/modules/websocket_streaming_client_module/src/websocket_streaming_client_module_impl.cpp new file mode 100644 index 0000000..26d2bbb --- /dev/null +++ b/modules/websocket_streaming_client_module/src/websocket_streaming_client_module_impl.cpp @@ -0,0 +1,343 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE + +static std::string WebsocketDeviceTypeId = "OpenDAQLTStreaming"; +static std::string OldWebsocketDeviceTypeId = "OpenDAQLTStreamingOld"; +static std::string WebsocketDevicePrefix = "daq.lt"; +static std::string OldWebsocketDevicePrefix = "daq.ws"; + +static const std::regex RegexIpv6Hostname(R"(^(.+://)?(\[[a-fA-F0-9:]+(?:\%[a-zA-Z0-9_\.-~]+)?\])(?::(\d+))?(/.*)?$)"); +static const std::regex RegexIpv4Hostname(R"(^(.+://)?([^:/\s]+)(?::(\d+))?(/.*)?$)"); + +using namespace discovery; +using namespace daq::websocket_streaming; + +WebsocketStreamingClientModule::WebsocketStreamingClientModule(ContextPtr context) + : Module("OpenDAQWebsocketClientModule", + daq::VersionInfo(WS_STREAM_CL_MODULE_MAJOR_VERSION, WS_STREAM_CL_MODULE_MINOR_VERSION, WS_STREAM_CL_MODULE_PATCH_VERSION), + std::move(context), + "OpenDAQWebsocketClientModule") + , deviceIndex(0) + , discoveryClient( + {"LT"} + ) +{ + discoveryClient.initMdnsClient(List("_streaming-lt._tcp.local.", "_streaming-ws._tcp.local.")); + loggerComponent = this->context.getLogger().getOrAddComponent("StreamingLTClient"); +} + +ListPtr WebsocketStreamingClientModule::onGetAvailableDevices() +{ + auto availableDevices = List(); + for (const auto& device : discoveryClient.discoverMdnsDevices()) + availableDevices.pushBack(populateDiscoveredDevice(device)); + return availableDevices; +} + +DictPtr WebsocketStreamingClientModule::onGetAvailableDeviceTypes() +{ + auto result = Dict(); + + const auto websocketDeviceType = createWebsocketDeviceType(false); + const auto oldWebsocketDeviceType = createWebsocketDeviceType(true); + + result.set(websocketDeviceType.getId(), websocketDeviceType); + result.set(oldWebsocketDeviceType.getId(), oldWebsocketDeviceType); + + return result; +} + +DictPtr WebsocketStreamingClientModule::onGetAvailableStreamingTypes() +{ + auto result = Dict(); + + auto websocketStreamingType = createWebsocketStreamingType(); + + result.set(websocketStreamingType.getId(), websocketStreamingType); + + return result; +} + +DevicePtr WebsocketStreamingClientModule::onCreateDevice(const StringPtr& connectionString, + const ComponentPtr& parent, + const PropertyObjectPtr& config) +{ + if (!connectionString.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException); + + if (!acceptsConnectionParameters(connectionString, config)) + DAQ_THROW_EXCEPTION(InvalidParameterException); + + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Context is not available."); + + // We don't create any streaming objects here since the + // internal streaming object is always created within the device + + const StringPtr strPtr = formConnectionString(connectionString, config); + const std::string str = strPtr; + + std::scoped_lock lock(sync); + + std::string localId = fmt::format("websocket_pseudo_device{}", deviceIndex++); + auto device = WebsocketClientDevice(context, parent, localId, strPtr); + + // Set the connection info for the device + auto host = String(""); + auto port = -1; + { + std::smatch match; + + bool parsed = false; + parsed = std::regex_search(str, match, RegexIpv6Hostname); + if (!parsed) + { + parsed = std::regex_search(str, match, RegexIpv4Hostname); + } + + if (parsed) + { + host = match[2].str(); + port = 7414; + if (match[3].matched) + port = std::stoi(match[3]); + } + } + + // Set the connection info for the device + ServerCapabilityConfigPtr connectionInfo = device.getInfo().getConfigurationConnectionInfo(); + connectionInfo.setProtocolId(WebsocketDeviceTypeId); + connectionInfo.setProtocolName("OpenDAQLTStreaming"); + connectionInfo.setProtocolType(ProtocolType::Streaming); + connectionInfo.setConnectionType("TCP/IP"); + connectionInfo.addAddress(host); + connectionInfo.setPort(port); + connectionInfo.setPrefix("daq.lt"); + connectionInfo.setConnectionString(strPtr); + + return device; +} + +bool WebsocketStreamingClientModule::acceptsConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& /*config*/) +{ + std::string connStr = connectionString; + auto found = connStr.find(WebsocketDevicePrefix + "://") == 0 || connStr.find(OldWebsocketDevicePrefix + "://") == 0; + return found; +} + +bool WebsocketStreamingClientModule::acceptsStreamingConnectionParameters(const StringPtr& connectionString, const PropertyObjectPtr& config) +{ + if (connectionString.assigned() && connectionString != "") + { + return acceptsConnectionParameters(connectionString, config); + } + return false; +} + +StreamingPtr WebsocketStreamingClientModule::onCreateStreaming(const StringPtr& connectionString, const PropertyObjectPtr& config) +{ + if (!connectionString.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException); + + if (!acceptsStreamingConnectionParameters(connectionString, config)) + DAQ_THROW_EXCEPTION(InvalidParameterException); + + const StringPtr str = formConnectionString(connectionString, config); + return WebsocketStreaming(str, context); +} + +Bool WebsocketStreamingClientModule::onCompleteServerCapability(const ServerCapabilityPtr& source, const ServerCapabilityConfigPtr& target) +{ + if (target.getProtocolId() != "OpenDAQLTStreaming") + return false; + + if (source.getConnectionType() != "TCP/IP") + return false; + + if (!source.getAddresses().assigned() || !source.getAddresses().getCount()) + { + LOG_W("Source server capability address is not available when filling in missing LT streaming 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 LT streaming capability information.") + return false; + } + + auto port = target.getPort(); + if (port == -1) + { + port = 7414; + target.setPort(port); + LOG_W("LT server capability is missing port. Defaulting to 7414.") + } + + 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; + + StringPtr connectionString; + if (source.getPrefix() == target.getPrefix()) + connectionString = addrInfo.getConnectionString(); + else + connectionString = createUrlConnectionString(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; +} + +StringPtr WebsocketStreamingClientModule::createUrlConnectionString(const StringPtr& host, + const IntegerPtr& port, + const StringPtr& path) +{ + return String(std::string(WebsocketDevicePrefix) + fmt::format("://{}:{}{}", host, port, path)); +} + +DeviceTypePtr WebsocketStreamingClientModule::createWebsocketDeviceType(bool useOldPrefix) +{ + const StringPtr prefix = useOldPrefix ? String(OldWebsocketDevicePrefix) : String(WebsocketDevicePrefix); + const StringPtr id = useOldPrefix ? String(OldWebsocketDeviceTypeId) : String(WebsocketDeviceTypeId); + + return DeviceTypeBuilder() + .setId(id) + .setName("Streaming LT enabled pseudo-device") + .setDescription("Pseudo device, provides only signals of the remote device as flat list") + .setConnectionStringPrefix(prefix) + .setDefaultConfig(createDefaultConfig()) + .build(); +} + +StreamingTypePtr WebsocketStreamingClientModule::createWebsocketStreamingType() +{ + return StreamingTypeBuilder() + .setId(WebsocketDeviceTypeId) + .setName("Streaming LT") + .setDescription("openDAQ native streaming protocol client") + .setConnectionStringPrefix("daq.lt") + .setDefaultConfig(createDefaultConfig()) + .build(); +} + +PropertyObjectPtr WebsocketStreamingClientModule::createDefaultConfig() +{ + auto obj = PropertyObject(); + obj.addProperty(IntProperty("Port", 7414)); + return obj; +} + +StringPtr WebsocketStreamingClientModule::formConnectionString(const StringPtr& connectionString, const PropertyObjectPtr& config) +{ + int port = 7414; + if (config.assigned() && config.hasProperty("Port")) + port = config.getPropertyValue("Port"); + + std::string urlString = connectionString.toStdString(); + std::smatch match; + + std::string host = ""; + std::string prefix = ""; + std::string path = "/"; + + bool parsed = false; + parsed = std::regex_search(urlString, match, RegexIpv6Hostname); + if (!parsed) + { + parsed = std::regex_search(urlString, match, RegexIpv4Hostname); + } + + if (parsed) + { + prefix = match[1]; + host = match[2]; + + if (match[3].matched) + port = std::stoi(match[3]); + + if (port == 7414) + return connectionString; + + if (match[4].matched) + path = match[4]; + + return prefix + host + ":" + std::to_string(port) + path; + } + + return connectionString; +} + +DeviceInfoPtr WebsocketStreamingClientModule::populateDiscoveredDevice(const MdnsDiscoveredDevice& discoveredDevice) +{ + auto cap = ServerCapability(WebsocketDeviceTypeId, "OpenDAQLTStreaming", ProtocolType::Streaming); + + for (const auto& ipAddress : discoveredDevice.ipv4Addresses) + { + auto connectionStringIpv4 = WebsocketStreamingClientModule::createUrlConnectionString( + ipAddress, + discoveredDevice.servicePort, + discoveredDevice.getPropertyOrDefault("path", "/") + ); + cap.addConnectionString(connectionStringIpv4); + cap.addAddress(ipAddress); + const auto addressInfo = AddressInfoBuilder().setAddress(ipAddress) + .setReachabilityStatus(AddressReachabilityStatus::Unknown) + .setType("IPv4") + .setConnectionString(connectionStringIpv4) + .build(); + cap.addAddressInfo(addressInfo); + } + + for (const auto& ipAddress : discoveredDevice.ipv6Addresses) + { + auto connectionStringIpv6 = WebsocketStreamingClientModule::createUrlConnectionString( + ipAddress, + discoveredDevice.servicePort, + discoveredDevice.getPropertyOrDefault("path", "/") + ); + cap.addConnectionString(connectionStringIpv6); + cap.addAddress(ipAddress); + + const auto addressInfo = AddressInfoBuilder().setAddress(ipAddress) + .setReachabilityStatus(AddressReachabilityStatus::Unknown) + .setType("IPv6") + .setConnectionString(connectionStringIpv6) + .build(); + cap.addAddressInfo(addressInfo); + } + + cap.setConnectionType("TCP/IP"); + cap.setPrefix("daq.lt"); + cap.setProtocolVersion(discoveredDevice.getPropertyOrDefault("protocolVersion", "")); + if (discoveredDevice.servicePort > 0) + cap.setPort(discoveredDevice.servicePort); + + return populateDiscoveredDeviceInfo(DiscoveryClient::populateDiscoveredInfoProperties, discoveredDevice, cap, createWebsocketDeviceType(false)); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_CLIENT_MODULE diff --git a/modules/websocket_streaming_client_module/tests/CMakeLists.txt b/modules/websocket_streaming_client_module/tests/CMakeLists.txt new file mode 100644 index 0000000..a251317 --- /dev/null +++ b/modules/websocket_streaming_client_module/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +set(MODULE_NAME ws_stream_cl_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_websocket_streaming_client_module.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${TEST_APP}coverage ${TEST_APP} ${TEST_APP}coverage) +endif() diff --git a/modules/websocket_streaming_client_module/tests/test_app.cpp b/modules/websocket_streaming_client_module/tests/test_app.cpp new file mode 100644 index 0000000..d92f41f --- /dev/null +++ b/modules/websocket_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/modules/websocket_streaming_client_module/tests/test_websocket_streaming_client_module.cpp b/modules/websocket_streaming_client_module/tests/test_websocket_streaming_client_module.cpp new file mode 100644 index 0000000..36782c2 --- /dev/null +++ b/modules/websocket_streaming_client_module/tests/test_websocket_streaming_client_module.cpp @@ -0,0 +1,192 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +using WebsocketStreamingClientModuleTest = testing::Test; +using namespace daq; + +static ModulePtr CreateModule() +{ + ModulePtr module; + createModule(&module, NullContext()); + return module; +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateModule) +{ + IModule* module = nullptr; + ErrCode errCode = createModule(&module, NullContext()); + ASSERT_TRUE(OPENDAQ_SUCCEEDED(errCode)); + + ASSERT_NE(module, nullptr); + module->releaseRef(); +} + +TEST_F(WebsocketStreamingClientModuleTest, ModuleName) +{ + auto module = CreateModule(); + ASSERT_EQ(module.getModuleInfo().getName(), "OpenDAQWebsocketClientModule"); +} + +TEST_F(WebsocketStreamingClientModuleTest, VersionAvailable) +{ + auto module = CreateModule(); + ASSERT_TRUE(module.getModuleInfo().getVersionInfo().assigned()); +} + +TEST_F(WebsocketStreamingClientModuleTest, VersionCorrect) +{ + auto module = CreateModule(); + auto version = module.getModuleInfo().getVersionInfo(); + + ASSERT_EQ(version.getMajor(), WS_STREAM_CL_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), WS_STREAM_CL_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), WS_STREAM_CL_MODULE_PATCH_VERSION); +} + +TEST_F(WebsocketStreamingClientModuleTest, EnumerateDevices) +{ + auto module = CreateModule(); + + ListPtr deviceInfo; + ASSERT_NO_THROW(deviceInfo = module.getAvailableDevices()); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateDeviceConnectionStringNull) +{ + auto module = CreateModule(); + + DevicePtr device; + ASSERT_THROW(device = module.createDevice(nullptr, nullptr), ArgumentNullException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateDeviceConnectionStringEmpty) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("", nullptr), InvalidParameterException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateDeviceConnectionStringInvalid) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("fdfdfdfdde", nullptr), InvalidParameterException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateDeviceConnectionStringInvalidId) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("daqref://devicett3axxr1", nullptr), InvalidParameterException); + ASSERT_THROW(module.createDevice("daq.opcua://devicett3axxr1", nullptr), InvalidParameterException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateDeviceConnectionFailed) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createDevice("daq.lt://127.0.0.1", nullptr), NotFoundException); +} + +//TEST_F(WebsocketStreamingClientModuleTest, CreateConnectionString) +//{ +// auto context = NullContext(); +// ModulePtr module; +// createModule(&module, context); +// +// StringPtr connectionString; +// +// ServerCapabilityConfigPtr serverCapabilityIgnored = ServerCapability("test", "test", ProtocolType::Unknown); +// ASSERT_NO_THROW(connectionString = module.createConnectionString(serverCapabilityIgnored)); +// ASSERT_FALSE(connectionString.assigned()); +// +// ServerCapabilityConfigPtr serverCapability = ServerCapability("OpenDAQLTStreaming", "OpenDAQLTStreaming", ProtocolType::Streaming); +// ASSERT_THROW(module.createConnectionString(serverCapability), InvalidParameterException); +// +// serverCapability.addAddress("123.123.123.123"); +// ASSERT_EQ(module.createConnectionString(serverCapability), "daq.lt://123.123.123.123:7414"); +// +// serverCapability.setPort(1234); +// ASSERT_NO_THROW(connectionString = module.createConnectionString(serverCapability)); +// ASSERT_EQ(connectionString, "daq.lt://123.123.123.123:1234"); +// +// serverCapability.addProperty(StringProperty("Path", "/path")); +// ASSERT_NO_THROW(connectionString = module.createConnectionString(serverCapability)); +// ASSERT_EQ(connectionString, "daq.lt://123.123.123.123:1234/path"); +//} + +TEST_F(WebsocketStreamingClientModuleTest, CreateStreamingWithNullArguments) +{ + auto module = CreateModule(); + + DevicePtr device; + ASSERT_THROW(device = module.createStreaming(nullptr, nullptr), ArgumentNullException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateStreamingWithConnectionStringEmpty) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createStreaming("", nullptr), InvalidParameterException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateStreamingConnectionStringInvalid) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createStreaming("fdfdfdfdde", nullptr), InvalidParameterException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateStreamingConnectionStringInvalidId) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createStreaming("daqref://devicett3axxr1", nullptr), InvalidParameterException); + ASSERT_THROW(module.createStreaming("daq.opcua://devicett3axxr1", nullptr), InvalidParameterException); + ASSERT_THROW(module.createStreaming("daq.lt://devicett3axxr1", nullptr), NotFoundException); +} + +TEST_F(WebsocketStreamingClientModuleTest, 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(), 2u); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQLTStreaming")); + ASSERT_EQ(deviceTypes.get("OpenDAQLTStreaming").getId(), "OpenDAQLTStreaming"); + ASSERT_TRUE(deviceTypes.hasKey("OpenDAQLTStreamingOld")); + ASSERT_EQ(deviceTypes.get("OpenDAQLTStreamingOld").getId(), "OpenDAQLTStreamingOld"); + + DictPtr serverTypes; + ASSERT_NO_THROW(serverTypes = module.getAvailableServerTypes()); + ASSERT_EQ(serverTypes.getCount(), 0u); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateFunctionBlockIdNull) +{ + auto module = CreateModule(); + + FunctionBlockPtr functionBlock; + ASSERT_THROW(functionBlock = module.createFunctionBlock(nullptr, nullptr, "fb"), ArgumentNullException); +} + +TEST_F(WebsocketStreamingClientModuleTest, CreateFunctionBlockIdEmpty) +{ + auto module = CreateModule(); + + ASSERT_THROW(module.createFunctionBlock("", nullptr, "fb"), NotFoundException); +} diff --git a/modules/websocket_streaming_server_module/CMakeLists.txt b/modules/websocket_streaming_server_module/CMakeLists.txt new file mode 100644 index 0000000..0b5b926 --- /dev/null +++ b/modules/websocket_streaming_server_module/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME) +project(WebsocketStreamingServerModule VERSION ${OPENDAQ_PACKAGE_VERSION} LANGUAGES CXX) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/common.h b/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/common.h new file mode 100644 index 0000000..e075ba1 --- /dev/null +++ b/modules/websocket_streaming_server_module/include/websocket_streaming_server_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_WEBSOCKET_STREAMING_SERVER_MODULE BEGIN_NAMESPACE_OPENDAQ_MODULE(websocket_streaming_server_module) +#define END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE END_NAMESPACE_OPENDAQ_MODULE diff --git a/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/module_dll.h b/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/module_dll.h new file mode 100644 index 0000000..b129905 --- /dev/null +++ b/modules/websocket_streaming_server_module/include/websocket_streaming_server_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(WebsocketStreamingServerModule) diff --git a/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/websocket_streaming_server_impl.h b/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/websocket_streaming_server_impl.h new file mode 100644 index 0000000..8e11392 --- /dev/null +++ b/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/websocket_streaming_server_impl.h @@ -0,0 +1,52 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE + +class WebsocketStreamingServerImpl : public Server +{ +public: + explicit WebsocketStreamingServerImpl(const DevicePtr& rootDevice, + const PropertyObjectPtr& config, + const ContextPtr& context); + static PropertyObjectPtr createDefaultConfig(const ContextPtr& context); + static ServerTypePtr createType(const ContextPtr& context); + static PropertyObjectPtr populateDefaultConfig(const PropertyObjectPtr& config, const ContextPtr& context); + +protected: + PropertyObjectPtr getDiscoveryConfig() override; + void onStopServer() override; + static void populateDefaultConfigFromProvider(const ContextPtr& context, const PropertyObjectPtr& config); + + daq::websocket_streaming::WebsocketStreamingServer websocketStreamingServer; +}; + +OPENDAQ_DECLARE_CLASS_FACTORY_WITH_INTERFACE( + INTERNAL_FACTORY, WebsocketStreamingServer, daq::IServer, + DevicePtr, rootDevice, + PropertyObjectPtr, config, + const ContextPtr&, context +) + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE diff --git a/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/websocket_streaming_server_module_impl.h b/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/websocket_streaming_server_module_impl.h new file mode 100644 index 0000000..2415e17 --- /dev/null +++ b/modules/websocket_streaming_server_module/include/websocket_streaming_server_module/websocket_streaming_server_module_impl.h @@ -0,0 +1,35 @@ +/* + * 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 + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE + +class WebsocketStreamingServerModule final : public Module +{ +public: + WebsocketStreamingServerModule(ContextPtr context); + + DictPtr onGetAvailableServerTypes() override; + ServerPtr onCreateServer(const StringPtr& serverType, const PropertyObjectPtr& serverConfig, const DevicePtr& rootDevice) override; + +private: + std::mutex sync; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE diff --git a/modules/websocket_streaming_server_module/src/CMakeLists.txt b/modules/websocket_streaming_server_module/src/CMakeLists.txt new file mode 100644 index 0000000..2f25750 --- /dev/null +++ b/modules/websocket_streaming_server_module/src/CMakeLists.txt @@ -0,0 +1,48 @@ +# Windows NSIS package manager limits lenght of variables +# "openDAQ_[MODULE_NAME]_[SUFFIX]" with 60 characters max +# The suffix with max lenght is [Development_was_installed] +# so the shorter module name [ws_stream_srv_module] should be used instead of full name: +# [ws_stream_srv_module] +set(LIB_NAME ws_stream_srv_module) +set(MODULE_HEADERS_DIR ../include/${TARGET_FOLDER_NAME}) + +set(SRC_Include common.h + module_dll.h + websocket_streaming_server_module_impl.h + websocket_streaming_server_impl.h +) + +set(SRC_Srcs module_dll.cpp + websocket_streaming_server_module_impl.cpp + websocket_streaming_server_impl.cpp +) + +prepend_include(${TARGET_FOLDER_NAME} SRC_Include) + +source_group("module" FILES ${MODULE_HEADERS_DIR}/websocket_streaming_server_module_impl.h + ${MODULE_HEADERS_DIR}/websocket_streaming_server_impl.h + ${MODULE_HEADERS_DIR}/module_dll.h + module_dll.cpp + websocket_streaming_server_module_impl.cpp + streaming_server_impl.cpp +) + + +add_library(${LIB_NAME} SHARED ${SRC_Include} + ${SRC_Srcs} +) + +add_library(${SDK_TARGET_NAMESPACE}::${LIB_NAME} ALIAS ${LIB_NAME}) + +target_link_libraries(${LIB_NAME} PUBLIC daq::opendaq + PRIVATE daq::websocket_streaming +) + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + $ +) + +opendaq_set_module_properties(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) +create_version_header(${LIB_NAME}) + diff --git a/modules/websocket_streaming_server_module/src/module_dll.cpp b/modules/websocket_streaming_server_module/src/module_dll.cpp new file mode 100644 index 0000000..86c098b --- /dev/null +++ b/modules/websocket_streaming_server_module/src/module_dll.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +using namespace daq::modules::websocket_streaming_server_module; + +DEFINE_MODULE_EXPORTS(WebsocketStreamingServerModule) + diff --git a/modules/websocket_streaming_server_module/src/websocket_streaming_server_impl.cpp b/modules/websocket_streaming_server_module/src/websocket_streaming_server_impl.cpp new file mode 100644 index 0000000..0371574 --- /dev/null +++ b/modules/websocket_streaming_server_module/src/websocket_streaming_server_impl.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE + +using namespace daq; + +WebsocketStreamingServerImpl::WebsocketStreamingServerImpl(const DevicePtr& rootDevice, + const PropertyObjectPtr& config, + const ContextPtr& context) + : Server("OpenDAQLTStreaming", config, rootDevice, context) + , websocketStreamingServer(rootDevice, context) +{ + const uint16_t streamingPort = config.getPropertyValue("WebsocketStreamingPort"); + const uint16_t controlPort = config.getPropertyValue("WebsocketControlPort"); + + websocketStreamingServer.setStreamingPort(streamingPort); + websocketStreamingServer.setControlPort(controlPort); + websocketStreamingServer.start(); +} + +void WebsocketStreamingServerImpl::populateDefaultConfigFromProvider(const ContextPtr& context, const PropertyObjectPtr& config) +{ + if (!context.assigned()) + return; + if (!config.assigned()) + return; + + auto options = context.getModuleOptions("StreamingLtServer"); + for (const auto& [key, value] : options) + { + if (config.hasProperty(key)) + { + config->setPropertyValue(key, value); + } + } +} + +PropertyObjectPtr WebsocketStreamingServerImpl::createDefaultConfig(const ContextPtr& context) +{ + constexpr Int minPortValue = 0; + constexpr Int maxPortValue = 65535; + + auto defaultConfig = PropertyObject(); + + const auto websocketPortProp = + IntPropertyBuilder("WebsocketStreamingPort", 7414).setMinValue(minPortValue).setMaxValue(maxPortValue).build(); + defaultConfig.addProperty(websocketPortProp); + + const auto websocketControlPortProp = + IntPropertyBuilder("WebsocketControlPort", 7438).setMinValue(minPortValue).setMaxValue(maxPortValue).build(); + defaultConfig.addProperty(websocketControlPortProp); + + defaultConfig.addProperty(StringProperty("Path", "/")); + + populateDefaultConfigFromProvider(context, defaultConfig); + return defaultConfig; +} + +PropertyObjectPtr WebsocketStreamingServerImpl::getDiscoveryConfig() +{ + auto discoveryConfig = PropertyObject(); + discoveryConfig.addProperty(StringProperty("ServiceName", "_streaming-lt._tcp.local.")); + discoveryConfig.addProperty(StringProperty("ServiceCap", "LT")); + discoveryConfig.addProperty(StringProperty("Path", config.getPropertyValue("Path"))); + discoveryConfig.addProperty(IntProperty("Port", config.getPropertyValue("WebsocketStreamingPort"))); + discoveryConfig.addProperty(StringProperty("ProtocolVersion", "")); + return discoveryConfig; +} + + +ServerTypePtr WebsocketStreamingServerImpl::createType(const ContextPtr& context) +{ + return ServerType( + "OpenDAQLTStreaming", + "openDAQ LT Streaming server", + "Publishes device signals as a flat list and streams data over WebsocketTcp protocol", + WebsocketStreamingServerImpl::createDefaultConfig(context)); +} + +void WebsocketStreamingServerImpl::onStopServer() +{ + websocketStreamingServer.stop(); +} + +PropertyObjectPtr WebsocketStreamingServerImpl::populateDefaultConfig(const PropertyObjectPtr& config, const ContextPtr& context) +{ + const auto defConfig = createDefaultConfig(context); + for (const auto& prop : defConfig.getAllProperties()) + { + const auto name = prop.getName(); + if (config.hasProperty(name)) + defConfig.setPropertyValue(name, config.getPropertyValue(name)); + } + + return defConfig; +} + +OPENDAQ_DEFINE_CLASS_FACTORY_WITH_INTERFACE( + INTERNAL_FACTORY, WebsocketStreamingServer, daq::IServer, + daq::DevicePtr, rootDevice, + PropertyObjectPtr, config, + const ContextPtr&, context +) + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE diff --git a/modules/websocket_streaming_server_module/src/websocket_streaming_server_module_impl.cpp b/modules/websocket_streaming_server_module/src/websocket_streaming_server_module_impl.cpp new file mode 100644 index 0000000..a614496 --- /dev/null +++ b/modules/websocket_streaming_server_module/src/websocket_streaming_server_module_impl.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE + +WebsocketStreamingServerModule::WebsocketStreamingServerModule(ContextPtr context) + : Module("OpenDAQWebsocketStreamingServerModule", + daq::VersionInfo(WS_STREAM_SRV_MODULE_MAJOR_VERSION, WS_STREAM_SRV_MODULE_MINOR_VERSION, WS_STREAM_SRV_MODULE_PATCH_VERSION), + std::move(context), + "OpenDAQWebsocketStreamingServerModule") +{ +} + +DictPtr WebsocketStreamingServerModule::onGetAvailableServerTypes() +{ + auto result = Dict(); + + auto serverType = WebsocketStreamingServerImpl::createType(context); + result.set(serverType.getId(), serverType); + + return result; +} + +ServerPtr WebsocketStreamingServerModule::onCreateServer(const StringPtr& serverType, + const PropertyObjectPtr& serverConfig, + const DevicePtr& rootDevice) +{ + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Context parameter cannot be null."); + + auto wsConfig = serverConfig; + if (!wsConfig.assigned()) + wsConfig = WebsocketStreamingServerImpl::createDefaultConfig(context); + else + wsConfig = WebsocketStreamingServerImpl::populateDefaultConfig(wsConfig, context); + + ServerPtr server(WebsocketStreamingServer_Create(rootDevice, wsConfig, context)); + return server; +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING_SERVER_MODULE diff --git a/modules/websocket_streaming_server_module/tests/CMakeLists.txt b/modules/websocket_streaming_server_module/tests/CMakeLists.txt new file mode 100644 index 0000000..02df280 --- /dev/null +++ b/modules/websocket_streaming_server_module/tests/CMakeLists.txt @@ -0,0 +1,28 @@ +set(MODULE_NAME ws_stream_srv_module) +set(TEST_APP test_${MODULE_NAME}) + +set(TEST_SOURCES test_websocket_streaming_server_module.cpp + test_app.cpp +) + +add_executable(${TEST_APP} ${TEST_SOURCES} +) + +target_link_libraries(${TEST_APP} PRIVATE daq::test_utils + daq::opendaq_mocks + ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + Taskflow::Taskflow +) + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if (MSVC) # Ignoring warning for the Taskflow + target_compile_options(${TEST_APP} PRIVATE /wd4324) +endif() + +if (OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${TEST_APP}coverage ${TEST_APP} ${TEST_APP}coverage) +endif() diff --git a/modules/websocket_streaming_server_module/tests/test_app.cpp b/modules/websocket_streaming_server_module/tests/test_app.cpp new file mode 100644 index 0000000..64dd0cc --- /dev/null +++ b/modules/websocket_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/modules/websocket_streaming_server_module/tests/test_websocket_streaming_server_module.cpp b/modules/websocket_streaming_server_module/tests/test_websocket_streaming_server_module.cpp new file mode 100644 index 0000000..65307c0 --- /dev/null +++ b/modules/websocket_streaming_server_module/tests/test_websocket_streaming_server_module.cpp @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class WebsocketStreamingServerModuleTest : public testing::Test +{ +public: + void TearDown() override + { + } +}; + +using namespace daq; + +static ModulePtr CreateModule(ContextPtr context = NullContext()) +{ + ModulePtr module; + createWebsocketStreamingServerModule(&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 deviceModule(MockDeviceModule_Create(context)); + moduleManager.addModule(deviceModule); + + const ModulePtr fbModule(MockFunctionBlockModule_Create(context)); + moduleManager.addModule(fbModule); + + const ModulePtr daqWebsocketStreamingServerModule = CreateModule(context); + moduleManager.addModule(daqWebsocketStreamingServerModule); + + 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("OpenDAQLTStreaming").createDefaultConfig(); + config.setPropertyValue("WebsocketStreamingPort", 0); + config.setPropertyValue("WebsocketControlPort", 0); + return config; +} + +TEST_F(WebsocketStreamingServerModuleTest, CreateModule) +{ + IModule* module = nullptr; + ErrCode errCode = createModule(&module, NullContext()); + ASSERT_TRUE(OPENDAQ_SUCCEEDED(errCode)); + + ASSERT_NE(module, nullptr); + module->releaseRef(); +} + +TEST_F(WebsocketStreamingServerModuleTest, ModuleName) +{ + auto module = CreateModule(); + ASSERT_EQ(module.getModuleInfo().getName(), "OpenDAQWebsocketStreamingServerModule"); +} + +TEST_F(WebsocketStreamingServerModuleTest, VersionAvailable) +{ + auto module = CreateModule(); + ASSERT_TRUE(module.getModuleInfo().getVersionInfo().assigned()); +} + +TEST_F(WebsocketStreamingServerModuleTest, VersionCorrect) +{ + auto module = CreateModule(); + auto version = module.getModuleInfo().getVersionInfo(); + + ASSERT_EQ(version.getMajor(), WS_STREAM_SRV_MODULE_MAJOR_VERSION); + ASSERT_EQ(version.getMinor(), WS_STREAM_SRV_MODULE_MINOR_VERSION); + ASSERT_EQ(version.getPatch(), WS_STREAM_SRV_MODULE_PATCH_VERSION); +} + +TEST_F(WebsocketStreamingServerModuleTest, 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("OpenDAQLTStreaming")); + ASSERT_EQ(serverTypes.get("OpenDAQLTStreaming").getId(), "OpenDAQLTStreaming"); +} + +TEST_F(WebsocketStreamingServerModuleTest, ServerConfig) +{ + auto module = CreateModule(); + + DictPtr serverTypes = module.getAvailableServerTypes(); + ASSERT_TRUE(serverTypes.hasKey("OpenDAQLTStreaming")); + auto config = serverTypes.get("OpenDAQLTStreaming").createDefaultConfig(); + ASSERT_TRUE(config.assigned()); + + ASSERT_TRUE(config.hasProperty("WebsocketStreamingPort")); + ASSERT_EQ(config.getPropertyValue("WebsocketStreamingPort"), 7414); + + ASSERT_TRUE(config.hasProperty("WebsocketControlPort")); + ASSERT_EQ(config.getPropertyValue("WebsocketControlPort"), 7438); +} + +TEST_F(WebsocketStreamingServerModuleTest, CreateServer) +{ + auto device = CreateTestInstance(); + auto module = CreateModule(device.getContext()); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(module.createServer("OpenDAQLTStreaming", device.getRootDevice(), config)); +} + +TEST_F(WebsocketStreamingServerModuleTest, CreateServerFromInstance) +{ + auto device = CreateTestInstance(); + auto config = CreateServerConfig(device); + + ASSERT_NO_THROW(device.addServer("OpenDAQLTStreaming", config)); +} diff --git a/shared/libraries/websocket_streaming/CMakeLists.txt b/shared/libraries/websocket_streaming/CMakeLists.txt new file mode 100644 index 0000000..a89b800 --- /dev/null +++ b/shared/libraries/websocket_streaming/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.10) +set_cmake_folder_context(TARGET_FOLDER_NAME ${SDK_TARGET_NAMESPACE}_websocket_streaming) +project(OpenDaqStreaming + VERSION 4.0.0 + LANGUAGES CXX +) + +add_subdirectory(src) + +if (OPENDAQ_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/async_packet_reader.h b/shared/libraries/websocket_streaming/include/websocket_streaming/async_packet_reader.h new file mode 100644 index 0000000..430b204 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/async_packet_reader.h @@ -0,0 +1,61 @@ +/* + * 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 "websocket_streaming/websocket_streaming.h" +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class AsyncPacketReader +{ +public: + using OnPacketCallback = std::function& packets)>; + + AsyncPacketReader(const DevicePtr& device, const ContextPtr& context); + ~AsyncPacketReader(); + + void start(); + void stop(); + void onPacket(const OnPacketCallback& callback); + void setLoopFrequency(uint32_t freqency); + void startReadSignals(const ListPtr& signals); + void stopReadSignals(const ListPtr& signals); + +protected: + void startReadThread(); + void createReaders(); + void addReader(SignalPtr signalToRead); + void removeReader(SignalPtr signalToRead); + + DevicePtr device; + ContextPtr context; + OnPacketCallback onPacketCallback; + std::thread readThread; + std::atomic readThreadStarted{false}; + std::chrono::milliseconds sleepTime; + std::vector> signalReaders; + + LoggerPtr logger; + LoggerComponentPtr loggerComponent; + std::mutex readersSync; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/input_signal.h b/shared/libraries/websocket_streaming/include/websocket_streaming/input_signal.h new file mode 100644 index 0000000..2c44004 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/input_signal.h @@ -0,0 +1,236 @@ +/* + * 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 +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class InputSignalBase; +using InputSignalBasePtr = std::shared_ptr; + +class InputSignal; +using InputSignalPtr = std::shared_ptr; + +class InputSignalBase +{ +public: + InputSignalBase(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + const InputSignalBasePtr& domainSignal, + daq::streaming_protocol::LogCallback logCb); + + virtual void processSamples(const NumberPtr& startDomainValue, const uint8_t* data, size_t sampleCount); + virtual DataPacketPtr generateDataPacket(const NumberPtr& packetOffset, + const uint8_t* data, + size_t dataSize, + size_t sampleCount, + const DataPacketPtr& domainPacket) = 0; + virtual bool isDomainSignal() const = 0; + virtual bool isCountable() const = 0; + + virtual EventPacketPtr createDecriptorChangedPacket(bool valueChanged = true, bool domainChanged = true) const; + + void setDataDescriptor(const DataDescriptorPtr& dataDescriptor); + virtual bool hasDescriptors() const; + + DataDescriptorPtr getSignalDescriptor() const; + std::string getTableId() const; + std::string getSignalId() const; + + InputSignalBasePtr getInputDomainSignal() const; + + void setSubscribed(bool subscribed); + bool getSubscribed(); + + const DataPacketPtr& getLastPacket() const noexcept; + void setLastPacket(const DataPacketPtr& packet); + +protected: + const std::string signalId; + const std::string tableId; + DataDescriptorPtr currentDataDescriptor; + const InputSignalBasePtr inputDomainSignal; + + std::string name; + std::string description; + + daq::streaming_protocol::LogCallback logCallback; + mutable std::mutex descriptorsSync; + bool subscribed; + + daq::DataPacketPtr lastPacket; +}; + +/// Used as a placeholder for uninitialized or incomplete signals which aren't supported by LT-streaming +class InputNullSignal : public InputSignalBase +{ +public: + InputNullSignal(const std::string& signalId, + streaming_protocol::LogCallback logCb); + + EventPacketPtr createDecriptorChangedPacket(bool valueChanged = true, bool domainChanged = true) const override; + bool hasDescriptors() const override; + + DataPacketPtr generateDataPacket(const NumberPtr& packetOffset, + const uint8_t* data, + size_t dataSize, + size_t sampleCount, + const DataPacketPtr& domainPacket) override; + bool isDomainSignal() const override; + bool isCountable() const override; +}; + +class InputDomainSignal : public InputSignalBase +{ +public: + InputDomainSignal(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + streaming_protocol::LogCallback logCb); + + DataPacketPtr generateDataPacket(const NumberPtr& packetOffset, + const uint8_t* data, + size_t dataSize, + size_t sampleCount, + const DataPacketPtr& domainPacket) override; + bool isDomainSignal() const override; + bool isCountable() const override; + +private: + DataPacketPtr lastDomainPacket; +}; + +class InputExplicitDataSignal : public InputSignalBase +{ +public: + InputExplicitDataSignal(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + const InputSignalBasePtr& domainSignal, + streaming_protocol::LogCallback logCb); + + DataPacketPtr generateDataPacket(const NumberPtr& packetOffset, + const uint8_t* data, + size_t dataSize, + size_t sampleCount, + const DataPacketPtr& domainPacket) override; + bool isDomainSignal() const override; + bool isCountable() const override; +}; + +class InputConstantDataSignal : public InputSignalBase +{ +public: + using SignalValueType = + std::variant; + + InputConstantDataSignal(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + const InputSignalBasePtr& domainSignal, + streaming_protocol::LogCallback logCb, + const nlohmann::json& metaInfoStartValue); + + void processSamples(const NumberPtr& absoluteStartDomainValue, const uint8_t* data, size_t sampleCount) override; + DataPacketPtr generateDataPacket(const NumberPtr& packetOffset, + const uint8_t* data, + size_t dataSize, + size_t sampleCount, + const DataPacketPtr& domainPacket) override; + bool isDomainSignal() const override; + bool isCountable() const override; + + void updateStartValue(const nlohmann::json& metaInfoStartValue); + +private: + using CachedSignalValues = std::map; + + template + static DataType convertToNumeric(const nlohmann::json& jsonNumeric); + + template + static auto callWithSampleType(daq::SampleType sampleType, Func&& func); + + template + static DataPacketPtr createTypedConstantPacket( + SignalValueType startValue, + const std::vector>& otherValues, + size_t sampleCount, + const DataPacketPtr& domainPacket, + const DataDescriptorPtr& dataDescriptor); + + NumberPtr calcDomainValue(const NumberPtr& startDomainValue, const uint64_t sampleIndex); + NumberPtr getDomainRuleDelta(); + uint32_t calcPosition(const NumberPtr& startDomainValue, const NumberPtr& domainValue); + CachedSignalValues::iterator insertDefaultValue(const NumberPtr& domainValue); + + CachedSignalValues cachedSignalValues; + std::optional defaultStartValue; + bool suppressDefaultStartValueWarnings; +}; + +inline InputSignalBasePtr InputSignal(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + bool isTimeSignal, + const InputSignalBasePtr& domainSignal, + streaming_protocol::LogCallback logCb, + const nlohmann::json& constRuleStartValueMeta) +{ + auto dataRuleType = signalInfo.dataDescriptor.getRule().getType(); + + if (isTimeSignal) + { + if (dataRuleType == daq::DataRuleType::Linear) + return std::make_shared(signalId, tabledId, signalInfo, logCb); + if (dataRuleType == daq::DataRuleType::Explicit) + return std::make_shared(signalId, tabledId, signalInfo, nullptr, logCb); + else + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unsupported input domain signal rule"); + } + else + { + if (dataRuleType == daq::DataRuleType::Explicit) + return std::make_shared(signalId, tabledId, signalInfo, domainSignal, logCb); + else if (dataRuleType == daq::DataRuleType::Constant) + return std::make_shared(signalId, tabledId, signalInfo, domainSignal, logCb, constRuleStartValueMeta); + else + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unsupported input data signal rule"); + } +} + +inline InputSignalBasePtr InputPlaceHolderSignal(const std::string& signalId, + streaming_protocol::LogCallback logCb) +{ + return std::make_shared(signalId, logCb); +} + +inline bool isPlaceHolderSignal(const InputSignalBasePtr& inputSignal) +{ + return (std::dynamic_pointer_cast(inputSignal)) ? true : false; +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/output_signal.h b/shared/libraries/websocket_streaming/include/websocket_streaming/output_signal.h new file mode 100644 index 0000000..c607633 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/output_signal.h @@ -0,0 +1,227 @@ +/* + * 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 "websocket_streaming/websocket_streaming.h" +#include +#include +#include +#include "streaming_protocol/Logging.hpp" +#include "websocket_streaming/signal_info.h" + +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class OutputSignalBase; +using OutputSignalBasePtr = std::shared_ptr; + +class OutputDomainSignalBase; +using OutputDomainSignalBasePtr = std::shared_ptr; + +class OutputSignalBase +{ +public: + OutputSignalBase(const SignalPtr& signal, + const DataDescriptorPtr& domainDescriptor, + daq::streaming_protocol::BaseSignalPtr stream, + daq::streaming_protocol::LogCallback logCb); + virtual ~OutputSignalBase(); + + virtual void writeDomainDescriptorChanges(const DataDescriptorPtr& valueDescriptor) = 0; + virtual void writeValueDescriptorChanges(const DataDescriptorPtr& domainDescriptor) = 0; + virtual void writeDaqDataPacket(const DataPacketPtr& packet) = 0; + virtual void setSubscribed(bool subscribed) = 0; + virtual bool isDataSignal() = 0; + + SignalPtr getDaqSignal(); + bool isSubscribed(); + + void submitTimeConfigChange(const DataDescriptorPtr& domainDescriptor); + bool isTimeConfigChanged(const DataDescriptorPtr& domainDescriptor); + +protected: + virtual void toStreamedSignal(const SignalPtr& signal, const SignalProps& sigProps) = 0; + + void submitSignalChanges(); + + static SignalProps getSignalProps(const SignalPtr& signal); + void writeDescriptorChangedEvent(const DataDescriptorPtr& descriptor); + + SignalPtr daqSignal; + SignalConfigPtr streamedDaqSignal; // used as dummy signal for encoding event packets + + daq::streaming_protocol::LogCallback logCallback; + + bool subscribed{false}; + bool doSetStartTime{false}; + std::mutex subscribedSync; + daq::streaming_protocol::BaseSignalPtr stream; + +private: + void processAttributeChangedCoreEvent(ComponentPtr& component, CoreEventArgsPtr& args); + void subscribeToCoreEvent(); + void unsubscribeFromCoreEvent(); + void createStreamedSignal(); + + DataDescriptorPtr domainDescriptor; +}; + +/// Used as a placeholder for openDAQ signals which aren't supported by LT-streaming +class OutputNullSignal : public OutputSignalBase +{ +public: + OutputNullSignal(const SignalPtr& signal, daq::streaming_protocol::LogCallback logCb); + + void writeDomainDescriptorChanges(const DataDescriptorPtr& valueDescriptor) override; + void writeValueDescriptorChanges(const DataDescriptorPtr& domainDescriptor) override; + void writeDaqDataPacket(const DataPacketPtr& packet) override; + void setSubscribed(bool subscribed) override; + bool isDataSignal() override; + +protected: + void toStreamedSignal(const SignalPtr& signal, const SignalProps& sigProps) override; +}; + +class OutputValueSignalBase : public OutputSignalBase +{ +public: + OutputValueSignalBase(daq::streaming_protocol::BaseValueSignalPtr valueStream, + const SignalPtr& signal, + OutputDomainSignalBasePtr outputDomainSignal, + daq::streaming_protocol::LogCallback logCb); + + void writeDomainDescriptorChanges(const DataDescriptorPtr& valueDescriptor) override; + void writeValueDescriptorChanges(const DataDescriptorPtr& domainDescriptor) override; + void writeDaqDataPacket(const DataPacketPtr& packet) override; + virtual void setSubscribed(bool subscribed) override; + bool isDataSignal() override; + +protected: + virtual void writeDataPacket(const DataPacketPtr& packet) = 0; + void toStreamedSignal(const SignalPtr& signal, const SignalProps& sigProps) override; + + OutputDomainSignalBasePtr outputDomainSignal; + +private: + daq::streaming_protocol::BaseValueSignalPtr valueStream; +}; + +class OutputDomainSignalBase : public OutputSignalBase +{ +public: + friend class OutputValueSignalBase; + friend class OutputConstValueSignal; + + OutputDomainSignalBase(daq::streaming_protocol::BaseDomainSignalPtr domainStream, + const SignalPtr& signal, + daq::streaming_protocol::LogCallback logCb); + + void writeDomainDescriptorChanges(const DataDescriptorPtr& valueDescriptor) override; + void writeValueDescriptorChanges(const DataDescriptorPtr& domainDescriptor) override; + void writeDaqDataPacket(const DataPacketPtr& packet) override; + void setSubscribed(bool subscribed) override; + bool isDataSignal() override; + + uint64_t calcStartTimeOffset(uint64_t dataPacketTimeStamp); + +private: + void subscribeByDataSignal(); + void unsubscribeByDataSignal(); + + size_t subscribedByDataSignalCount{0}; + daq::streaming_protocol::BaseDomainSignalPtr domainStream; +}; + +class OutputSyncValueSignal : public OutputValueSignalBase +{ +public: + OutputSyncValueSignal(const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + OutputDomainSignalBasePtr outputDomainSignal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb); + +protected: + void writeDataPacket(const DataPacketPtr& packet) override; + +private: + static daq::streaming_protocol::BaseSynchronousSignalPtr createSignalStream( + const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb); + + daq::streaming_protocol::BaseSynchronousSignalPtr syncStream; +}; + +class OutputConstValueSignal : public OutputValueSignalBase +{ +public: + using ConstantValueType = + std::variant; + + OutputConstValueSignal(const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + OutputDomainSignalBasePtr outputDomainSignal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb); + + void setSubscribed(bool subscribed) override; + +protected: + void writeDataPacket(const DataPacketPtr& packet) override; + +private: + static daq::streaming_protocol::BaseConstantSignalPtr createSignalStream( + const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb); + + template + static std::vector> + extractConstValuesFromDataPacket(const DataPacketPtr& packet); + + template + void writeData(const DataPacketPtr& packet, uint64_t firstValueIndex); + + daq::streaming_protocol::BaseConstantSignalPtr constStream; + std::optional lastConstValue; +}; + +class OutputLinearDomainSignal : public OutputDomainSignalBase +{ +public: + OutputLinearDomainSignal(const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb); + +protected: + void toStreamedSignal(const SignalPtr& signal, const SignalProps& sigProps) override; + +private: + static daq::streaming_protocol::LinearTimeSignalPtr createSignalStream( + const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb); + + daq::streaming_protocol::LinearTimeSignalPtr linearStream; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/signal_descriptor_converter.h b/shared/libraries/websocket_streaming/include/websocket_streaming/signal_descriptor_converter.h new file mode 100644 index 0000000..6ceb232 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/signal_descriptor_converter.h @@ -0,0 +1,64 @@ +/* + * 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 "websocket_streaming/websocket_streaming.h" +#include "websocket_streaming/signal_info.h" +#include +#include +#include +#include +#include "streaming_protocol/SubscribedSignal.hpp" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class SignalDescriptorConverter +{ +public: + /** + * @param subscribedSignal The object holding everything about thee signal on the consumer side + * @throws ConversionFailedException + */ + static SubscribedSignalInfo ToDataDescriptor( + const daq::streaming_protocol::SubscribedSignal& subscribedSignal, + const daq::ContextPtr& context); + /** + * @throws ConversionFailedException + */ + static void ToStreamedValueSignal(const daq::SignalPtr& valueSignal, + daq::streaming_protocol::BaseValueSignalPtr valueStream, + const SignalProps& sigProps); + static void ToStreamedLinearSignal(const daq::SignalPtr& domainSignal, + streaming_protocol::LinearTimeSignalPtr linearStream, + const SignalProps& sigProps); + + static void EncodeInterpretationObject(const DataDescriptorPtr& dataDescriptor, nlohmann::json& extra); + +private: + static daq::DataRulePtr GetRule(const daq::streaming_protocol::SubscribedSignal& subscribedSignal); + static void SetLinearTimeRule(const daq::DataRulePtr& rule, daq::streaming_protocol::LinearTimeSignalPtr linearStream); + static daq::SampleType Convert(daq::streaming_protocol::SampleType dataType); + static daq::SampleType ConvertSampleTypeString(const std::string& sampleType); + static daq::streaming_protocol::SampleType Convert(daq::SampleType sampleType); + static daq::RangePtr CreateDefaultRange(daq::SampleType sampleType); + static void DecodeInterpretationObject(const nlohmann::json& extra, DataDescriptorBuilderPtr& dataDescriptorBuilder); + static void DecodeBitsInterpretationObject(const nlohmann::json& bits, DataDescriptorBuilderPtr& dataDescriptorBuilder); + static nlohmann::json DictToJson(const DictPtr& dict); + static ObjectPtr JsonToObject(const nlohmann::json& json); +}; +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/signal_info.h b/shared/libraries/websocket_streaming/include/websocket_streaming/signal_info.h new file mode 100644 index 0000000..b60884f --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/signal_info.h @@ -0,0 +1,39 @@ +/* + * 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 "websocket_streaming/websocket_streaming.h" +#include + +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +struct SignalProps +{ + std::optional name; + std::optional description; +}; + +struct SubscribedSignalInfo +{ + DataDescriptorPtr dataDescriptor; + SignalProps signalProps; + std::string signalName; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/streaming_client.h b/shared/libraries/websocket_streaming/include/websocket_streaming/streaming_client.h new file mode 100644 index 0000000..0ab4e31 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/streaming_client.h @@ -0,0 +1,161 @@ +/* + * 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 "websocket_streaming/websocket_streaming.h" +#include "websocket_streaming/input_signal.h" +#include "websocket_streaming/signal_info.h" +#include "stream/WebsocketClientStream.hpp" +#include "streaming_protocol/ProtocolHandler.hpp" +#include "streaming_protocol/SubscribedSignal.hpp" +#include "opendaq/signal_factory.h" +#include "streaming_protocol/Logging.hpp" +#include +#include +#include +#include + +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class StreamingClient; +using StreamingClientPtr = std::shared_ptr; + +class StreamingClient +{ +public: + using OnPacketCallback = std::function; + using OnSignalCallback = std::function; + using OnFindSignalCallback = std::function; + using OnDomainSignalInitCallback = + std::function; + using OnAvailableSignalsCallback = std::function& signalIds)>; + using OnSubsciptionAckCallback = std::function; + using OnSignalsInitDoneCallback = std::function; + + StreamingClient(const ContextPtr& context, const std::string& connectionString, bool useRawTcpConnection = false); + StreamingClient(const ContextPtr& context, const std::string& host, uint16_t port, const std::string& target, bool useRawTcpConnection = false); + ~StreamingClient(); + + bool connect(); + void disconnect(); + + void onDeviceAvailableSignalInit(const OnSignalCallback& callback); + void onDeviceSignalUpdated(const OnSignalCallback& callback); + void onDeviceDomainSingalInit(const OnDomainSignalInitCallback& callback); + void onDeviceAvailableSignals(const OnAvailableSignalsCallback& callback); + void onDeviceUnavailableSignals(const OnAvailableSignalsCallback& callback); + void onDeviceHiddenSignal(const OnSignalCallback& callback); + void onDeviceSignalsInitDone(const OnSignalsInitDoneCallback& callback); + + void onStreamingAvailableSignals(const OnAvailableSignalsCallback& callback); + void onStreamingUnavailableSignals(const OnAvailableSignalsCallback& callback); + void onStreamingHiddenSignal(const OnSignalCallback& callback); + void onPacket(const OnPacketCallback& callack); + void onSubscriptionAck(const OnSubsciptionAckCallback& callback); + + std::string getHost(); + uint16_t getPort(); + std::string getTarget(); + bool isConnected(); + void setConnectTimeout(std::chrono::milliseconds timeout); + void subscribeSignal(const std::string& signalId); + void unsubscribeSignal(const std::string& signalId); + +protected: + void parseConnectionString(const std::string& url); + void stopBackgroundContext(); + + void onSignalMeta(const daq::streaming_protocol::SubscribedSignal& subscribedSignal, + const std::string& method, + const nlohmann::json& params); + void onProtocolMeta(daq::streaming_protocol::ProtocolHandler& protocolHandler, const std::string& method, const nlohmann::json& params); + void onMessage(const daq::streaming_protocol::SubscribedSignal& subscribedSignal, uint64_t timeStamp, const uint8_t* data, size_t valueCount); + void setDataSignal(const daq::streaming_protocol::SubscribedSignal& subscribedSignal, const ContextPtr& context); + void setTimeSignal(const daq::streaming_protocol::SubscribedSignal& subscribedSignal, const ContextPtr& context); + void publishSignalChanges(const InputSignalBasePtr& signal, bool valueChanged, bool domainChanged); + void onSignal(const daq::streaming_protocol::SubscribedSignal& subscribedSignal, const nlohmann::json& params); + void setSignalInitSatisfied(const std::string& signalId); + std::vector findDataSignalsByTableId(const std::string& tableId); + InputSignalBasePtr findTimeSignalByTableId(const std::string& tableId); + void checkTmpSubscribedSignalsInit(); + void unavailableSignalsHandler(const nlohmann::json::const_iterator& unavailableSignalsArray); + void availableSignalsHandler(const nlohmann::json::const_iterator& availableSignalsArray); + + ContextPtr context; + LoggerPtr logger; + LoggerComponentPtr loggerComponent; + daq::streaming_protocol::LogCallback logCallback; + + std::string host; + uint16_t port; + std::string target; + bool connected = false; + boost::asio::io_context ioContext; + boost::asio::io_context backgroundContext; + + daq::streaming_protocol::SignalContainer signalContainer; + daq::streaming_protocol::ProtocolHanlderPtr protocolHandler; + std::unordered_map availableSignals; + std::unordered_map hiddenSignals; + + // streaming callbacks + OnAvailableSignalsCallback onAvailableStreamingSignalsCb = [](const std::vector& signalIds) {}; + OnAvailableSignalsCallback onUnavailableStreamingSignalsCb = [](const std::vector& signalIds) {}; + OnSignalCallback onHiddenStreamingSignalCb = [](const StringPtr& signalId, const SubscribedSignalInfo&) {}; + OnSubsciptionAckCallback onSubscriptionAckCallback = [](const StringPtr& signalId, bool subscribed) {}; + OnPacketCallback onPacketCallback = [](const StringPtr&, const PacketPtr&) {}; + + // device callbacks + OnDomainSignalInitCallback onDomainSignalInitCallback = [](const StringPtr&, const StringPtr&) {}; + OnAvailableSignalsCallback onAvailableDeviceSignalsCb = [](const std::vector& signalIds) {}; + OnAvailableSignalsCallback onUnavailableDeviceSignalsCb = [](const std::vector& signalIds) {}; + OnSignalCallback onAvailableSignalInitCb = [](const StringPtr&, const SubscribedSignalInfo&) {}; + OnSignalCallback onSignalUpdatedCallback = [](const StringPtr& signalId, const SubscribedSignalInfo&) {}; + OnSignalCallback onHiddenDeviceSignalInitCb = [](const StringPtr& signalId, const SubscribedSignalInfo&) {}; + OnSignalsInitDoneCallback onSignalsInitDone = []() {}; + + std::thread clientIoThread; + std::thread clientBackgroundThread; + std::mutex clientMutex; + std::condition_variable conditionVariable; + std::chrono::milliseconds connectTimeout{1000}; + + // signal meta-information (signal description, tableId, related signals, etc.) + // is published only for subscribed signals. + // as workaround we temporarily subscribe all signals to receive signal meta-info + // at initialization stage. + // To manage this the 'availableSigInitStatus' is used, it is map of 3-element tuples, where: + // 1-st is std::promise + // 2-nd is std::future + // 3-rd: boolean flag indicating that initial unsubscription completion ack is filtered-out + std::unordered_map, std::future, bool>> availableSigInitStatus; + + bool useRawTcpConnection; + + std::unordered_set tmpSubscribedSignalIds; + + void startBackgroundContext(); +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/streaming_server.h b/shared/libraries/websocket_streaming/include/websocket_streaming/streaming_server.h new file mode 100644 index 0000000..e83ea9e --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/streaming_server.h @@ -0,0 +1,148 @@ +/* + * 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 "websocket_streaming/websocket_streaming.h" +#include "stream/WebsocketServer.hpp" +#include "websocket_streaming/output_signal.h" +#include "streaming_protocol/StreamWriter.h" +#include "streaming_protocol/ControlServer.hpp" +#include "streaming_protocol/Logging.hpp" +#include +#include +#include + +#include +#include + +#include +#include +#include + + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class StreamingServer; +using StreamingServerPtr = std::shared_ptr; + +class StreamingServer +{ +public: + using OnAcceptCallback = std::function(const daq::streaming_protocol::StreamWriterPtr& writer)>; + using OnStartSignalsReadCallback = std::function& signal)>; + using OnStopSignalsReadCallback = std::function& signal)>; + using OnClientConnectedCallback = std::function; + using OnClientDisconnectedCallback = std::function; + + StreamingServer(const ContextPtr& context); + ~StreamingServer(); + + void start(uint16_t port = daq::streaming_protocol::WEBSOCKET_LISTENING_PORT, + uint16_t controlPort = daq::streaming_protocol::HTTP_CONTROL_PORT); + void stop(); + + void onAccept(const OnAcceptCallback& callback); + void onStartSignalsRead(const OnStartSignalsReadCallback& callback); + void onStopSignalsRead(const OnStopSignalsReadCallback& callback); + void onClientConnected(const OnClientConnectedCallback& callback); + void onClientDisconnected(const OnClientDisconnectedCallback& callback); + + void broadcastPacket(const std::string& signalId, const PacketPtr& packet); + + void addSignals(const ListPtr& signals); + void removeComponentSignals(const StringPtr& componentId); + void updateComponentSignals(const DictPtr& signals, const StringPtr& componentId); + +protected: + using SignalMap = std::unordered_map; + using ClientMap = std::unordered_map>; + + void doRead(const std::string& clientId, const stream::StreamPtr& stream); + void onReadDone(const std::string& clientId, + const stream::StreamPtr& stream, + const boost::system::error_code& ec, + std::size_t bytesRead); + void removeClient(const std::string& clientId); + + void addToOutputSignals(const SignalPtr& signal, + SignalMap& outputSignals, + const streaming_protocol::StreamWriterPtr& writer); + OutputDomainSignalBasePtr addUpdateOrFindDomainSignal(const SignalPtr& domainSignal, + SignalMap& outputSignals, + const streaming_protocol::StreamWriterPtr& writer); + + void publishSignalsToClient(const streaming_protocol::StreamWriterPtr& writer, + const ListPtr& signals, + SignalMap& outputSignals); + void onAcceptInternal(const daq::stream::StreamPtr& stream); + OutputDomainSignalBasePtr createOutputDomainSignal(const SignalPtr& daqDomainSignal, + const std::string& tableId, + const streaming_protocol::StreamWriterPtr& writer); + OutputSignalBasePtr createOutputValueSignal(const SignalPtr& daqSignal, + const OutputDomainSignalBasePtr& outputDomainSignal, + const std::string& tableId, + const streaming_protocol::StreamWriterPtr& writer); + void handleDataDescriptorChanges(OutputSignalBasePtr& outputSignal, + SignalMap& outputSignals, + const streaming_protocol::StreamWriterPtr& writer, + const EventPacketPtr& packet); + + void updateOutputPlaceholderSignal(OutputSignalBasePtr& outputSignal, + SignalMap& outputSignals, + const streaming_protocol::StreamWriterPtr& writer, + bool subscribed); + + void writeProtocolInfo(const daq::streaming_protocol::StreamWriterPtr& writer); + void writeSignalsAvailable(const daq::streaming_protocol::StreamWriterPtr& writer, + const std::vector& signalIds); + void writeSignalsUnavailable(const daq::streaming_protocol::StreamWriterPtr& writer, + const std::vector& signalIds); + void writeInit(const daq::streaming_protocol::StreamWriterPtr& writer); + + bool isSignalSubscribed(const std::string& signalId) const; + bool subscribeHandler(const std::string& signalId, OutputSignalBasePtr signal); + bool unsubscribeHandler(const std::string& signalId, OutputSignalBasePtr signal); + int onControlCommand(const std::string& streamId, + const std::string& command, + const daq::streaming_protocol::SignalIds& signalIds, + std::string& errorMessage); + + void startReadSignals(const ListPtr& signals); + void stopReadSignals(const ListPtr& signals); + + uint16_t port; + boost::asio::io_context ioContext; + boost::asio::executor_work_guard work; + daq::stream::WebsocketServerUniquePtr server; + std::unique_ptr controlServer; + std::thread serverThread; + ClientMap clients; + OnAcceptCallback onAcceptCallback; + OnStartSignalsReadCallback onStartSignalsReadCallback; + OnStopSignalsReadCallback onStopSignalsReadCallback; + OnClientConnectedCallback clientConnectedHandler; + OnClientDisconnectedCallback clientDisconnectedHandler; + LoggerPtr logger; + LoggerComponentPtr loggerComponent; + daq::streaming_protocol::LogCallback logCallback; + bool serverRunning{false}; + std::mutex sync; + +private: + static DataRuleType getSignalRuleType(const SignalPtr& domainSignal); +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_device_factory.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_device_factory.h new file mode 100644 index 0000000..183082c --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_device_factory.h @@ -0,0 +1,32 @@ +/* + * 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 "websocket_streaming/websocket_client_device_impl.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +inline DevicePtr WebsocketClientDevice(const ContextPtr& context, + const ComponentPtr& parent, + const StringPtr& localId, + const StringPtr& connectionString) +{ + DevicePtr obj(createWithImplementation(context, parent, localId, connectionString)); + return obj; +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_device_impl.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_device_impl.h new file mode 100644 index 0000000..f0fa615 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_device_impl.h @@ -0,0 +1,56 @@ +/* + * 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 "websocket_streaming/streaming_client.h" +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class WebsocketClientDeviceImpl : public Device +{ +public: + explicit WebsocketClientDeviceImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const StringPtr& connectionString); + +protected: + void removed() override; + + DeviceInfoPtr onGetInfo() override; + void createWebsocketStreaming(); + void activateStreaming(); + void updateSignalProperties(const SignalPtr& signal, const SubscribedSignalInfo& sInfo); + void onSignalInit(const StringPtr& signalId, const SubscribedSignalInfo& sInfo); + void onSignalUpdated(const StringPtr& signalId, const SubscribedSignalInfo& sInfo); + void onDomainSignalInit(const StringPtr& signalId, const StringPtr& domainSignalId); + void registerAvailableSignals(const std::vector& signalIds); + void removeSignals(const std::vector& signalIds); + void registerHiddenSignal(const StringPtr& signalId, const SubscribedSignalInfo& sInfo); + void addInitializedSignals(); + + DeviceInfoConfigPtr deviceInfo; + std::unordered_map deviceSignals; + std::vector orderedSignalIds; + StringPtr connectionString; + + StreamingPtr websocketStreaming; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_signal_factory.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_signal_factory.h new file mode 100644 index 0000000..83570bc --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_signal_factory.h @@ -0,0 +1,31 @@ +/* + * 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 "websocket_streaming/websocket_client_signal_impl.h" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +inline SignalPtr WebsocketClientSignal(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& streamingId) +{ + SignalPtr obj(createWithImplementation(ctx, + parent, + streamingId)); + return obj; +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_signal_impl.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_signal_impl.h new file mode 100644 index 0000000..5ed3efe --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_client_signal_impl.h @@ -0,0 +1,44 @@ +/* + * 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 "websocket_streaming/websocket_streaming.h" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class WebsocketClientSignalImpl final : public MirroredSignal +{ +public: + explicit WebsocketClientSignalImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& streamingId); + + StringPtr onGetRemoteId() const override; + Bool onTriggerEvent(const EventPacketPtr& eventPacket) override; + +protected: + SignalPtr onGetDomainSignal() override; + DataDescriptorPtr onGetDescriptor() override; + +private: + static StringPtr CreateLocalId(const StringPtr& streamingId); + + StringPtr streamingId; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming.h new file mode 100644 index 0000000..bbbad8c --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming.h @@ -0,0 +1,70 @@ +/* + * 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 + + +#define BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING \ + namespace daq::websocket_streaming \ + { +#define END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING } + +#include + +namespace daq::streaming_protocol +{ + static const uint16_t WEBSOCKET_LISTENING_PORT = 7414; + static const uint16_t HTTP_CONTROL_PORT = 7438; + class SubscribedSignal; + using SubscribedSignalPtr = std::shared_ptr; + + class ProtocolHandler; + using ProtocolHanlderPtr = std::shared_ptr; + + class StreamWriter; + using StreamWriterPtr = std::shared_ptr; + + class BaseSignal; + using BaseSignalPtr = std::shared_ptr; + + class BaseValueSignal; + using BaseValueSignalPtr = std::shared_ptr; + + class BaseSynchronousSignal; + using BaseSynchronousSignalPtr = std::shared_ptr; + + class BaseDomainSignal; + using BaseDomainSignalPtr = std::shared_ptr; + + class LinearTimeSignal; + using LinearTimeSignalPtr = std::shared_ptr; + + class BaseConstantSignal; + using BaseConstantSignalPtr = std::shared_ptr; +} + +namespace daq::stream +{ + class Stream; + using StreamPtr = std::shared_ptr; + + class WebsocketServer; + using WebsocketServerPtr = std::shared_ptr; + using WebsocketServerUniquePtr = std::unique_ptr; + + class WebsocketClientStream; + using WebsocketClientStreamPtr = std::shared_ptr; +} diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_factory.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_factory.h new file mode 100644 index 0000000..6c82b0b --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_factory.h @@ -0,0 +1,38 @@ +/* + * 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 "websocket_streaming/websocket_streaming_impl.h" +#include "websocket_streaming/streaming_client.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +inline StreamingPtr WebsocketStreaming(const StringPtr& connectionString, + const ContextPtr& context) +{ + StreamingPtr obj(createWithImplementation(connectionString, context)); + return obj; +} + +inline StreamingPtr WebsocketStreaming(const StreamingClientPtr& streamingClient, + const StringPtr& connectionString, + const ContextPtr& context) +{ + StreamingPtr obj(createWithImplementation(streamingClient, connectionString, context)); + return obj; +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_impl.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_impl.h new file mode 100644 index 0000000..c4a3695 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_impl.h @@ -0,0 +1,51 @@ +/* + * 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 "websocket_streaming/streaming_client.h" +#include + +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class WebsocketStreamingImpl : public Streaming +{ +public: + explicit WebsocketStreamingImpl(const StringPtr& connectionString, + const ContextPtr& context); + + explicit WebsocketStreamingImpl(StreamingClientPtr streamingClient, + const StringPtr& connectionString, + const ContextPtr& context); +protected: + void onSetActive(bool active) override; + void onAddSignal(const MirroredSignalConfigPtr& signal) override; + void onRemoveSignal(const MirroredSignalConfigPtr& signal) override; + void onSubscribeSignal(const StringPtr& signalStreamingId) override; + void onUnsubscribeSignal(const StringPtr& signalStreamingId) override; + + void prepareStreamingClient(); + void onAvailableSignals(const std::vector& signalIds); + void onUnavailableSignals(const std::vector& signalIds); + void onHiddenSignal(const std::string& signalId); + + daq::websocket_streaming::StreamingClientPtr streamingClient; + std::unordered_set hiddenSignals; +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_server.h b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_server.h new file mode 100644 index 0000000..ae04828 --- /dev/null +++ b/shared/libraries/websocket_streaming/include/websocket_streaming/websocket_streaming_server.h @@ -0,0 +1,58 @@ +/* + * 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 "websocket_streaming/streaming_server.h" +#include "websocket_streaming/async_packet_reader.h" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +class WebsocketStreamingServer +{ +public: + WebsocketStreamingServer(const InstancePtr& instance); + WebsocketStreamingServer(const DevicePtr& device, const ContextPtr& context); + ~WebsocketStreamingServer(); + + void setStreamingPort(uint16_t port); + void setControlPort(uint16_t port); + void start(); + void stop(); + +protected: + void componentAdded(ComponentPtr& sender, CoreEventArgsPtr& eventArgs); + void componentRemoved(ComponentPtr& sender, CoreEventArgsPtr& eventArgs); + void componentUpdated(ComponentPtr& updatedComponent); + void coreEventCallback(ComponentPtr& sender, CoreEventArgsPtr& eventArgs); + + DevicePtr device; + ContextPtr context; + + uint16_t streamingPort = 0; + uint16_t controlPort = 0; + daq::websocket_streaming::StreamingServer streamingServer; + daq::websocket_streaming::AsyncPacketReader packetReader; + LoggerComponentPtr loggerComponent; + std::unordered_map registeredClientIds; + +private: + static DictPtr getSignalsOfComponent(ComponentPtr& component); + void stopInternal(); +}; + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/CMakeLists.txt b/shared/libraries/websocket_streaming/src/CMakeLists.txt new file mode 100644 index 0000000..495ed9a --- /dev/null +++ b/shared/libraries/websocket_streaming/src/CMakeLists.txt @@ -0,0 +1,80 @@ +set(BASE_NAME websocket_streaming) +set(LIB_NAME ${SDK_TARGET_NAME}_${BASE_NAME}) + +set(SRC_Cpp signal_descriptor_converter.cpp + streaming_client.cpp + streaming_server.cpp + input_signal.cpp + output_signal.cpp + async_packet_reader.cpp + websocket_streaming_server.cpp + websocket_client_device_impl.cpp + websocket_client_signal_impl.cpp + websocket_streaming_impl.cpp +) + +set(SRC_PublicHeaders + websocket_streaming.h + signal_info.h + signal_descriptor_converter.h + streaming_client.h + streaming_server.h + input_signal.h + output_signal.h + async_packet_reader.h + websocket_streaming_server.h + websocket_client_device_impl.h + websocket_client_device_factory.h + websocket_client_signal_impl.h + websocket_client_signal_factory.h + websocket_streaming_impl.h + websocket_streaming_factory.h +) + +set(INCLUDE_DIR ../include/websocket_streaming) +prepend_include(${INCLUDE_DIR} SRC_PublicHeaders) + + +set(SRC_PrivateHeaders +) + + +add_library(${LIB_NAME} STATIC ${SRC_Cpp} + ${SRC_PublicHeaders} + ${SRC_PrivateHeaders} +) + +add_library(${SDK_TARGET_NAMESPACE}::${BASE_NAME} ALIAS ${LIB_NAME}) + +if(BUILD_64Bit OR BUILD_ARM) + set_target_properties(${LIB_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) +else() + set_target_properties(${LIB_NAME} PROPERTIES POSITION_INDEPENDENT_CODE OFF) +endif() + +target_include_directories(${LIB_NAME} PUBLIC $ + $ + + $ +) + +target_link_libraries(${LIB_NAME} PUBLIC daq::streaming_protocol + daq::opendaq +) + +# Fix daq::streaming_protocol` not properly propagating linking requirements on Windows +if (WIN32) + target_link_libraries(${LIB_NAME} + PUBLIC + ws2_32 + wsock32 + ) +endif() + +if (MSVC) + target_compile_options(${LIB_NAME} PRIVATE /bigobj) +endif() + +set_target_properties(${LIB_NAME} PROPERTIES PUBLIC_HEADER "${SRC_PublicHeaders}") + +opendaq_set_output_lib_name(${LIB_NAME} ${PROJECT_VERSION_MAJOR}) diff --git a/shared/libraries/websocket_streaming/src/async_packet_reader.cpp b/shared/libraries/websocket_streaming/src/async_packet_reader.cpp new file mode 100644 index 0000000..c48b839 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/async_packet_reader.cpp @@ -0,0 +1,146 @@ +#include "websocket_streaming/async_packet_reader.h" +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +AsyncPacketReader::AsyncPacketReader(const DevicePtr& device, const ContextPtr& context) + : device(device) + , context(context) + , logger(context.getLogger()) + , loggerComponent(logger.getOrAddComponent("WebsocketStreamingPacketReader")) +{ + setLoopFrequency(50); + onPacketCallback = [](const SignalPtr& signal, const ListPtr& packets) {}; +} + +AsyncPacketReader::~AsyncPacketReader() +{ + stop(); +} + +void AsyncPacketReader::start() +{ + readThreadStarted = true; + this->readThread = std::thread([this]() + { + this->startReadThread(); + LOG_I("Reading thread finished"); + }); +} + +void AsyncPacketReader::stop() +{ + onPacketCallback = [](const SignalPtr& signal, const ListPtr& packets) {}; + readThreadStarted = false; + if (readThread.joinable()) + { + readThread.join(); + LOG_I("Reading thread joined"); + } + + signalReaders.clear(); +} + +void AsyncPacketReader::onPacket(const OnPacketCallback& callback) +{ + onPacketCallback = callback; +} + +void AsyncPacketReader::setLoopFrequency(uint32_t freqency) +{ + uint64_t sleepMs = 1000.0 / freqency; + this->sleepTime = std::chrono::milliseconds(sleepMs); +} + +void AsyncPacketReader::startReadThread() +{ + while (readThreadStarted) + { + { + std::scoped_lock lock(readersSync); + bool hasPacketsToRead; + do + { + hasPacketsToRead = false; + for (const auto& [signal, reader] : signalReaders) + { + if (reader.getAvailableCount() == 0) + continue; + + const auto& packet = reader.read(); + onPacketCallback(signal, {packet}); + + if (reader.getAvailableCount() > 0) + hasPacketsToRead = true; + } + } + while(hasPacketsToRead); + } + + std::this_thread::sleep_for(sleepTime); + } +} + +void AsyncPacketReader::createReaders() +{ + signalReaders.clear(); + auto signals = device.getSignals(search::Recursive(search::Any())); + + for (const auto& signal : signals) + { + addReader(signal); + } +} + +void AsyncPacketReader::startReadSignals(const ListPtr& signals) +{ + std::scoped_lock lock(readersSync); + for (const auto& signal : signals) + addReader(signal); +} + +void AsyncPacketReader::stopReadSignals(const ListPtr& signals) +{ + std::scoped_lock lock(readersSync); + for (const auto& signal : signals) + removeReader(signal); +} + +void AsyncPacketReader::addReader(SignalPtr signalToRead) +{ + if (!signalToRead.getPublic()) + return; + + auto it = std::find_if(signalReaders.begin(), + signalReaders.end(), + [&signalToRead](const std::pair& element) + { + return element.first == signalToRead; + }); + if (it != signalReaders.end()) + return; + + LOG_I("Add reader for signal {}", signalToRead.getGlobalId()); + auto reader = PacketReader(signalToRead); + signalReaders.push_back(std::pair({signalToRead, reader})); +} + +void AsyncPacketReader::removeReader(SignalPtr signalToRead) +{ + auto it = std::find_if(signalReaders.begin(), + signalReaders.end(), + [&signalToRead](const std::pair& element) + { + return element.first == signalToRead; + }); + if (it == signalReaders.end()) + return; + + LOG_I("Remove reader for signal {}", signalToRead.getGlobalId()); + signalReaders.erase(it); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + diff --git a/shared/libraries/websocket_streaming/src/input_signal.cpp b/shared/libraries/websocket_streaming/src/input_signal.cpp new file mode 100644 index 0000000..09a6352 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/input_signal.cpp @@ -0,0 +1,520 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +using namespace daq; +using namespace daq::streaming_protocol; + +InputSignalBase::InputSignalBase(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + const InputSignalBasePtr& domainSignal, + streaming_protocol::LogCallback logCb) + : signalId(signalId) + , tableId(tabledId) + , currentDataDescriptor(signalInfo.dataDescriptor) + , inputDomainSignal(domainSignal) + , name(signalInfo.signalProps.name.value_or(signalInfo.signalName)) + , description(signalInfo.signalProps.description.value_or("")) + , logCallback(logCb) + , subscribed(false) +{ +} + +void InputSignalBase::processSamples(const NumberPtr& /*startDomainValue*/, const uint8_t* /*data*/, size_t /*sampleCount*/) +{ +} + +EventPacketPtr InputSignalBase::createDecriptorChangedPacket(bool valueChanged, bool domainChanged) const +{ + std::scoped_lock lock(descriptorsSync); + + if (isDomainSignal()) + { + const auto valueDescParam = descriptorToEventPacketParam(currentDataDescriptor); + return DataDescriptorChangedEventPacket(valueChanged ? valueDescParam : nullptr, nullptr); + } + else + { + const auto valueDescParam = descriptorToEventPacketParam(currentDataDescriptor); + const auto domainDesc = inputDomainSignal->getSignalDescriptor(); + const auto domainDescParam = descriptorToEventPacketParam(domainDesc); + return DataDescriptorChangedEventPacket(valueChanged ? valueDescParam : nullptr, + domainChanged ? domainDescParam : nullptr); + } +} + +void InputSignalBase::setDataDescriptor(const DataDescriptorPtr& dataDescriptor) +{ + std::scoped_lock lock(descriptorsSync); + + currentDataDescriptor = dataDescriptor; +} + +bool InputSignalBase::hasDescriptors() const +{ + std::scoped_lock lock(descriptorsSync); + + if (isDomainSignal()) + return currentDataDescriptor.assigned(); + else + return currentDataDescriptor.assigned() && + inputDomainSignal && inputDomainSignal->getSignalDescriptor().assigned(); +} + +DataDescriptorPtr InputSignalBase::getSignalDescriptor() const +{ + std::scoped_lock lock(descriptorsSync); + return currentDataDescriptor; +} + +std::string InputSignalBase::getTableId() const +{ + return tableId; +} + +std::string InputSignalBase::getSignalId() const +{ + return signalId; +} + +InputSignalBasePtr InputSignalBase::getInputDomainSignal() const +{ + return inputDomainSignal; +} + +void InputSignalBase::setSubscribed(bool subscribed) +{ + this->subscribed = subscribed; +} + +bool InputSignalBase::getSubscribed() +{ + return subscribed; +} + +const DataPacketPtr& InputSignalBase::getLastPacket() const noexcept +{ + return lastPacket; +} + +void InputSignalBase::setLastPacket(const DataPacketPtr& packet) +{ + lastPacket = packet; +} + +InputDomainSignal::InputDomainSignal(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + streaming_protocol::LogCallback logCb) + : InputSignalBase(signalId, tabledId, signalInfo, nullptr, logCb) +{ +} + +DataPacketPtr InputDomainSignal::generateDataPacket(const NumberPtr& packetOffset, + const uint8_t* /*data*/, + size_t dataSize, + size_t sampleCount, + const DataPacketPtr& /*domainPacket*/) +{ + std::scoped_lock lock(descriptorsSync); + + if (!lastDomainPacket.assigned() || lastDomainPacket.getOffset() != packetOffset) + { + lastDomainPacket = DataPacket(currentDataDescriptor, sampleCount, packetOffset); + } + + return lastDomainPacket; +} + +bool InputDomainSignal::isDomainSignal() const +{ + return true; +} + +bool InputDomainSignal::isCountable() const +{ + return false; +} + +InputExplicitDataSignal::InputExplicitDataSignal(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + const InputSignalBasePtr& domainSignal, + streaming_protocol::LogCallback logCb) + : InputSignalBase(signalId, tabledId, signalInfo, domainSignal, logCb) +{ +} + +DataPacketPtr InputExplicitDataSignal::generateDataPacket(const NumberPtr& /*packetOffset*/, + const uint8_t* data, + size_t dataSize, + size_t sampleCount, + const DataPacketPtr& domainPacket) +{ + std::scoped_lock lock(descriptorsSync); + + auto dataPacket = DataPacketWithDomain(domainPacket, currentDataDescriptor, sampleCount); + if (dataSize == dataPacket.getRawDataSize()) + std::memcpy(dataPacket.getRawData(), data, dataSize); + else + STREAMING_PROTOCOL_LOG_E("Provided streaming protocol packet data for signal {} has the wrong size: {} instead of {} (sample count: {})", + signalId, + dataSize, + dataPacket.getRawDataSize(), + sampleCount); + return dataPacket; +} + +bool InputExplicitDataSignal::isDomainSignal() const +{ + return !inputDomainSignal; +} + +bool InputExplicitDataSignal::isCountable() const +{ + return true; +} + +template +auto InputConstantDataSignal::callWithSampleType(daq::SampleType sampleType, Func&& func) +{ + switch (sampleType) + { + case daq::SampleType::Int8: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::Int16: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::Int32: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::Int64: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::UInt8: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::UInt16: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::UInt32: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::UInt64: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::Float32: + return func(daq::SampleTypeToType::Type{}); + case daq::SampleType::Float64: + return func(daq::SampleTypeToType::Type{}); + default: + throw std::invalid_argument("Unsupported sample type"); + } +} + +InputConstantDataSignal::InputConstantDataSignal(const std::string& signalId, + const std::string& tabledId, + const SubscribedSignalInfo& signalInfo, + const InputSignalBasePtr& domainSignal, + streaming_protocol::LogCallback logCb, + const nlohmann::json& metaInfoStartValue) + : InputSignalBase(signalId, tabledId, signalInfo, domainSignal, logCb) + , suppressDefaultStartValueWarnings(false) +{ + updateStartValue(metaInfoStartValue); +} + +NumberPtr InputConstantDataSignal::calcDomainValue(const NumberPtr& startDomainValue, const uint64_t sampleIndex) +{ + NumberPtr domainRuleDelta = getDomainRuleDelta(); + + if (startDomainValue.getCoreType() == CoreType::ctFloat) + return startDomainValue.getFloatValue() + sampleIndex * domainRuleDelta.getFloatValue(); + else + return startDomainValue.getIntValue() + sampleIndex * domainRuleDelta.getIntValue(); +} + +void InputConstantDataSignal::processSamples(const NumberPtr& absoluteStartDomainValue, const uint8_t* data, size_t sampleCount) +{ + std::scoped_lock lock(descriptorsSync); + + auto sampleType = currentDataDescriptor.getSampleType(); + const auto sampleSize = getSampleSize(sampleType); + const auto bufferSize = sampleCount * (sampleSize + sizeof(uint64_t)); + + for (size_t addrOffset = 0; addrOffset < bufferSize; addrOffset += sizeof(uint64_t) + sampleSize) + { + const uint64_t* pIndex = reinterpret_cast(data + addrOffset); + const uint8_t* pSignalValue = data + addrOffset + sizeof(uint64_t); + auto domainValue = calcDomainValue(absoluteStartDomainValue, *pIndex); + + try + { + auto extractConstantValue = [pSignalValue](const auto& typeTag) + { + using DataType = typename std::decay_t; + return SignalValueType(*(reinterpret_cast(pSignalValue))); + }; + SignalValueType signalValue = callWithSampleType(sampleType, extractConstantValue); + cachedSignalValues.insert_or_assign(domainValue, signalValue); + } + catch (...) + { + return; + } + } +} + +NumberPtr InputConstantDataSignal::getDomainRuleDelta() +{ + return inputDomainSignal->getSignalDescriptor().getRule().getParameters().get("delta"); +} + +uint32_t InputConstantDataSignal::calcPosition(const NumberPtr& startDomainValue, const NumberPtr& domainValue) +{ + NumberPtr domainRuleDelta = getDomainRuleDelta(); + + if (startDomainValue.getCoreType() == CoreType::ctFloat) + return (domainValue.getFloatValue() - startDomainValue.getFloatValue()) / domainRuleDelta.getFloatValue(); + else + return (domainValue.getIntValue() - startDomainValue.getIntValue()) / domainRuleDelta.getIntValue(); +} + +InputConstantDataSignal::CachedSignalValues::iterator InputConstantDataSignal::insertDefaultValue(const NumberPtr& domainValue) +{ + if (!suppressDefaultStartValueWarnings) + { + if (defaultStartValue.has_value()) + { + STREAMING_PROTOCOL_LOG_W("Constant rule signal id \"{}\" (table \"{}\"): " + "packet start value isn't yet received, will use default start value from meta-info", + this->signalId, + this->tableId); + } + else + { + STREAMING_PROTOCOL_LOG_W("Constant rule signal id \"{}\" (table \"{}\"): " + "packet start value isn't yet received nor valid one provided in meta-info, will use default start value 0", + this->signalId, + this->tableId); + } + suppressDefaultStartValueWarnings = true; // log warning message just ones + } + + auto createZeroValue = [](const auto& typeTag) + { + using DataType = typename std::decay_t; + return SignalValueType(static_cast(0)); + }; + SignalValueType zeroValue = callWithSampleType(currentDataDescriptor.getSampleType(), createZeroValue); + const auto result = cachedSignalValues.insert_or_assign(domainValue, defaultStartValue.value_or(zeroValue)); + return result.first; +} + +DataPacketPtr InputConstantDataSignal::generateDataPacket(const NumberPtr& /*packetOffset*/, + const uint8_t* /*data*/, + size_t /*dataSize*/, + size_t sampleCount, + const DataPacketPtr& domainPacket) +{ + if (sampleCount == 0) + return nullptr; + + std::scoped_lock lock(descriptorsSync); + + if (domainPacket.getDataDescriptor() != inputDomainSignal->getSignalDescriptor()) + { + STREAMING_PROTOCOL_LOG_E("Fail to generate constant data packet: domain descriptor mismatch"); + return nullptr; + } + + NumberPtr packetStartDomainValue = domainPacket.getOffset(); + NumberPtr packetEndDomainValue = calcDomainValue(packetStartDomainValue, sampleCount - 1); + + // search for cached value to be used as packet start value + // it should have smaller or equal domain value (map key) in comparison with packet domain value + auto itStart = cachedSignalValues.end(); + for (auto it = cachedSignalValues.begin(); it != cachedSignalValues.end(); ++it) + { + if (it->first <= packetStartDomainValue) + itStart = it; + } + + bool removeDefaultValueFromCache = false; + // appropriate start value is not found as it wasn't received as signal data + // temporary insert default value into cache and use it to generate packet + if (itStart == cachedSignalValues.end()) + { + try + { + itStart = insertDefaultValue(packetStartDomainValue); + removeDefaultValueFromCache = true; + } + catch (const std::exception& e) + { + STREAMING_PROTOCOL_LOG_E("Fail to generate constant data packet: {}", e.what()); + return nullptr; + } + } + + // start value found + SignalValueType packetStartValue = itStart->second; + + // search for other cached values which belong to generated packet domain values range + std::vector> packetOtherValues; + { + auto it = itStart; + ++it; + for (; it != cachedSignalValues.end() && it->first <= packetEndDomainValue; ++it) + { + auto pos = calcPosition(packetStartDomainValue, it->first); + packetOtherValues.emplace_back(pos, it->second); + } + } + + // erase values related to previously generated packets + if (itStart != cachedSignalValues.begin()) + { + cachedSignalValues.erase(cachedSignalValues.begin(), itStart); + } + // erase temporary inserted default value + if (removeDefaultValueFromCache) + { + cachedSignalValues.erase(itStart); + } + + try + { + auto createPacket = [&](const auto& typeTag) + { + using DataType = typename std::decay_t; + return createTypedConstantPacket(packetStartValue, packetOtherValues, sampleCount, domainPacket, currentDataDescriptor); + }; + return callWithSampleType(currentDataDescriptor.getSampleType(), createPacket); + } + catch (const std::exception& e) + { + STREAMING_PROTOCOL_LOG_E("Fail to generate constant data packet: {}", e.what()); + return nullptr; + } +} + +bool InputConstantDataSignal::isDomainSignal() const +{ + return false; +} + +bool InputConstantDataSignal::isCountable() const +{ + return false; +} + +void InputConstantDataSignal::updateStartValue(const nlohmann::json& metaInfoStartValue) +{ + std::scoped_lock lock(descriptorsSync); + + try + { + auto getValueFromJson = [&metaInfoStartValue](const auto& typeTag) + { + using DataType = typename std::decay_t; + return SignalValueType(convertToNumeric(metaInfoStartValue)); + }; + defaultStartValue = callWithSampleType(currentDataDescriptor.getSampleType(), getValueFromJson); + suppressDefaultStartValueWarnings = false; + } + catch (const std::exception& e) + { + STREAMING_PROTOCOL_LOG_I("Cannot get default start value from signal meta-info: {}", e.what()); + } +} + +template +DataType InputConstantDataSignal::convertToNumeric(const nlohmann::json& jsonNumeric) +{ + if (jsonNumeric.is_null()) + throw std::invalid_argument("No value provided"); + + if (!jsonNumeric.is_number()) + throw std::invalid_argument("JSON value is not number"); + + if constexpr (std::is_floating_point::value) + { + double numeric = jsonNumeric.get(); + if (numeric < std::numeric_limits::min() || numeric > std::numeric_limits::max()) + throw std::out_of_range("Value out of range"); + return static_cast(numeric); + } + else if constexpr (std::is_signed::value) + { + int64_t numeric = jsonNumeric.get(); + if (numeric < std::numeric_limits::min() || numeric > std::numeric_limits::max()) + throw std::out_of_range("Value out of range"); + return static_cast(numeric); + } + else if constexpr (std::is_unsigned::value) + { + uint64_t numeric = jsonNumeric.get(); + if (numeric > std::numeric_limits::max()) + throw std::out_of_range("Value out of range"); + return static_cast(numeric); + } + throw std::invalid_argument("Conversion failed - invalid sample type"); +} + +template +DataPacketPtr InputConstantDataSignal::createTypedConstantPacket( + SignalValueType startValue, + const std::vector>& otherValues, + size_t sampleCount, + const DataPacketPtr& domainPacket, + const DataDescriptorPtr& dataDescriptor) +{ + const auto startValueTyped = std::get(startValue); + std::vector> otherValuesTyped; + for (const auto& otherValue : otherValues) + { + const auto otherValueTyped = std::get(otherValue.second); + uint32_t pos = otherValue.first; + otherValuesTyped.push_back({pos, otherValueTyped}); + } + + return ConstantDataPacketWithDomain(domainPacket, dataDescriptor, sampleCount, startValueTyped, otherValuesTyped); +} + +InputNullSignal::InputNullSignal(const std::string& signalId, streaming_protocol::LogCallback logCb) + : InputSignalBase(signalId, std::string(), SubscribedSignalInfo(), nullptr, logCb) +{ +} + +EventPacketPtr InputNullSignal::createDecriptorChangedPacket(bool valueChanged, bool domainChanged) const +{ + return DataDescriptorChangedEventPacket(valueChanged ? NullDataDescriptor() : nullptr, + domainChanged ? NullDataDescriptor() : nullptr); +} + +bool InputNullSignal::hasDescriptors() const +{ + return true; +} + +DataPacketPtr InputNullSignal::generateDataPacket(const NumberPtr& /*packetOffset*/, + const uint8_t* /*data*/, + size_t /*dataSize*/, + size_t /*sampleCount*/, + const DataPacketPtr& /*domainPacket*/) +{ + return nullptr; +} + +bool InputNullSignal::isDomainSignal() const +{ + return false; +} + +bool InputNullSignal::isCountable() const +{ + return false; +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/output_signal.cpp b/shared/libraries/websocket_streaming/src/output_signal.cpp new file mode 100644 index 0000000..33d55a4 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/output_signal.cpp @@ -0,0 +1,787 @@ +#include "websocket_streaming/output_signal.h" +#include +#include "streaming_protocol/StreamWriter.h" +#include "streaming_protocol/SynchronousSignal.hpp" +#include "streaming_protocol/LinearTimeSignal.hpp" +#include "streaming_protocol/ConstantSignal.hpp" +#include +#include +#include +#include "websocket_streaming/signal_descriptor_converter.h" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +using namespace daq::streaming_protocol; +using namespace daq::stream; + +OutputSignalBase::OutputSignalBase(const SignalPtr& signal, + const DataDescriptorPtr& domainDescriptor, + BaseSignalPtr stream, + daq::streaming_protocol::LogCallback logCb) + : daqSignal(signal) + , logCallback(logCb) + , subscribed(false) + , stream(stream) + , domainDescriptor(domainDescriptor) +{ + createStreamedSignal(); + subscribeToCoreEvent(); +} + +OutputSignalBase::~OutputSignalBase() +{ + unsubscribeFromCoreEvent(); +} + +void OutputSignalBase::createStreamedSignal() +{ + const auto context = daqSignal.getContext(); + + streamedDaqSignal = SignalWithDescriptor(context, daqSignal.getDescriptor(), nullptr, daqSignal.getLocalId()); + + streamedDaqSignal.setName(daqSignal.getName()); + streamedDaqSignal.setDescription(daqSignal.getDescription()); +} + +void OutputSignalBase::subscribeToCoreEvent() +{ + daqSignal.getOnComponentCoreEvent() += event(this, &OutputSignalBase::processAttributeChangedCoreEvent); +} + +void OutputSignalBase::unsubscribeFromCoreEvent() +{ + daqSignal.getOnComponentCoreEvent() -= event(this, &OutputSignalBase::processAttributeChangedCoreEvent); +} + +void OutputSignalBase::processAttributeChangedCoreEvent(ComponentPtr& /*component*/, CoreEventArgsPtr& args) +{ + if (args.getEventId() != static_cast(CoreEventId::AttributeChanged)) + return; + + const auto params = args.getParameters(); + const auto name = params.get("AttributeName"); + const auto value = params.get(name); + + SignalProps sigProps; + if (name == "Name") + { + sigProps.name = value; + streamedDaqSignal.setName(value); + } + else if (name == "Description") + { + sigProps.description = value; + streamedDaqSignal.setDescription(value); + } + else + { + return; + } + + // Streaming LT does not support attribute change forwarding for active, public, and visible + toStreamedSignal(daqSignal, sigProps); + + std::scoped_lock lock(subscribedSync); + if(this->subscribed) + submitSignalChanges(); +} + +SignalProps OutputSignalBase::getSignalProps(const SignalPtr& signal) +{ + SignalProps signalProps; + + signalProps.name = signal.getName(); + signalProps.description = signal.getDescription(); + + return signalProps; +} + +SignalPtr OutputSignalBase::getDaqSignal() +{ + return daqSignal; +} + +bool OutputSignalBase::isSubscribed() +{ + std::scoped_lock lock(subscribedSync); + + return subscribed; +} + +void OutputSignalBase::submitSignalChanges() +{ + if (stream) + stream->writeSignalMetaInformation(); +} + +void OutputSignalBase::writeDescriptorChangedEvent(const DataDescriptorPtr& descriptor) +{ + streamedDaqSignal.setDescriptor(descriptor); + + toStreamedSignal(streamedDaqSignal, getSignalProps(streamedDaqSignal)); + submitSignalChanges(); +} + +void OutputSignalBase::submitTimeConfigChange(const DataDescriptorPtr& domainDescriptor) +{ + // significant parameters of domain signal have been changed + // reset values used for timestamp calculations + + this->domainDescriptor = domainDescriptor; + doSetStartTime = true; +} + +bool OutputSignalBase::isTimeConfigChanged(const DataDescriptorPtr& domainDescriptor) +{ + return this->domainDescriptor.getRule() != domainDescriptor.getRule() || + this->domainDescriptor.getTickResolution() != domainDescriptor.getTickResolution(); +} + +OutputValueSignalBase::OutputValueSignalBase(daq::streaming_protocol::BaseValueSignalPtr valueStream, + const SignalPtr& signal, + OutputDomainSignalBasePtr outputDomainSignal, + daq::streaming_protocol::LogCallback logCb) + : OutputSignalBase(signal, outputDomainSignal->getDaqSignal().getDescriptor(), valueStream, logCb) + , outputDomainSignal(outputDomainSignal) + , valueStream(valueStream) +{ +} + +void OutputValueSignalBase::writeDaqDataPacket(const DataPacketPtr& packet) +{ + std::scoped_lock lock(subscribedSync); + + if (!this->subscribed) + return; + + writeDataPacket(packet); +} + +void OutputValueSignalBase::writeValueDescriptorChanges(const DataDescriptorPtr& valueDescriptor) +{ + std::scoped_lock lock(subscribedSync); + + if (!this->subscribed) + return; + + if (valueDescriptor.assigned()) + { + this->writeDescriptorChangedEvent(valueDescriptor); + } + else + { + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unassigned value descriptor"); + } +} + +void OutputValueSignalBase::writeDomainDescriptorChanges(const DataDescriptorPtr& domainDescriptor) +{ + std::scoped_lock lock(subscribedSync); + + if (!this->subscribed) + return; + + if (domainDescriptor.assigned()) + { + if (outputDomainSignal->isTimeConfigChanged(domainDescriptor)) + { + outputDomainSignal->submitTimeConfigChange(domainDescriptor); + } + + if (isTimeConfigChanged(domainDescriptor)) + { + submitTimeConfigChange(domainDescriptor); + } + outputDomainSignal->writeDescriptorChangedEvent(domainDescriptor); + } + else + { + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unassigned domain descriptor"); + } +} + +// bool subscribed; / void setSubscribed(bool); / bool isSubscribed() : +// To prevent client side streaming protocol error the server should not send data before +// the subscribe acknowledgment is sent neither after the unsubscribe acknowledgment is sent. +// The `subscribed` member variable functions as a flag that enables/disables data streaming +// within the OutputSignal. +// Mutex locking ensures that data is sent only when the specified conditions are satisfied. +void OutputValueSignalBase::setSubscribed(bool subscribed) +{ + std::scoped_lock lock(subscribedSync); + + if (this->subscribed != subscribed) + { + this->subscribed = subscribed; + doSetStartTime = true; + if (subscribed) + { + outputDomainSignal->subscribeByDataSignal(); + stream->subscribe(); + } + else + { + stream->unsubscribe(); + outputDomainSignal->unsubscribeByDataSignal(); + } + } +} + +bool OutputValueSignalBase::isDataSignal() +{ + return true; +} + +void OutputValueSignalBase::toStreamedSignal(const SignalPtr& signal, const SignalProps& sigProps) +{ + SignalDescriptorConverter::ToStreamedValueSignal(signal, valueStream, sigProps); +} + +OutputDomainSignalBase::OutputDomainSignalBase(daq::streaming_protocol::BaseDomainSignalPtr domainStream, + const SignalPtr& signal, + daq::streaming_protocol::LogCallback logCb) + : OutputSignalBase(signal, signal.getDescriptor(), domainStream, logCb) + , domainStream(domainStream) +{ +} + +void OutputDomainSignalBase::writeDaqDataPacket(const DataPacketPtr& packet) +{ + DAQ_THROW_EXCEPTION(InvalidOperationException, "Streaming-lt: explicit streaming of domain signals is not supported"); +} + +void OutputDomainSignalBase::writeDomainDescriptorChanges(const DataDescriptorPtr& valueDescriptor) +{ + DAQ_THROW_EXCEPTION(InvalidOperationException, "Streaming-lt: explicit streaming of domain signals is not supported"); +} + +void OutputDomainSignalBase::writeValueDescriptorChanges(const DataDescriptorPtr& domainDescriptor) +{ + DAQ_THROW_EXCEPTION(InvalidOperationException, "Streaming-lt: explicit streaming of domain signals is not supported"); +} + +uint64_t OutputDomainSignalBase::calcStartTimeOffset(uint64_t dataPacketTimeStamp) +{ + if (doSetStartTime) + { + STREAMING_PROTOCOL_LOG_I("time signal {}: reset start timestamp: {}", daqSignal.getGlobalId(), dataPacketTimeStamp); + + domainStream->setTimeStart(dataPacketTimeStamp); + doSetStartTime = false; + return 0; + } + else + { + auto signalStartTime = domainStream->getTimeStart(); + if (dataPacketTimeStamp < signalStartTime) + { + STREAMING_PROTOCOL_LOG_E( + "Unable to calc start time index: domain signal start time {}, time stamp from packet {}", + signalStartTime, + dataPacketTimeStamp); + return 0; + } + return (dataPacketTimeStamp - signalStartTime); + } +} + +// bool subscribed; / void setSubscribed(bool); / bool isSubscribed(); / (un)subscribeByDataSignal() : +// To prevent client side streaming protocol error the server should not send data before +// the subscribe acknowledgment is sent neither after the unsubscribe acknowledgment is sent. +// The `subscribed` member variable functions as a flag that enables/disables data streaming +// within the OutputSignal. +// Mutex locking ensures that data is sent only when the specified conditions are satisfied. +void OutputDomainSignalBase::subscribeByDataSignal() +{ + std::scoped_lock lock(subscribedSync); + + if (subscribedByDataSignalCount == 0) + { + doSetStartTime = true; + if (!this->subscribed) + { + stream->subscribe(); + } + } + + subscribedByDataSignalCount++; +} + +void OutputDomainSignalBase::unsubscribeByDataSignal() +{ + std::scoped_lock lock(subscribedSync); + + if (subscribedByDataSignalCount == 0) + { + STREAMING_PROTOCOL_LOG_E("Cannot unsubscribe domain signal by data signal - already has 0 subscribers"); + return; + } + + subscribedByDataSignalCount--; + + if (subscribedByDataSignalCount == 0) + { + if (!this->subscribed) + { + stream->unsubscribe(); + } + } +} + +void OutputDomainSignalBase::setSubscribed(bool subscribed) +{ + std::scoped_lock lock(subscribedSync); + + if (this->subscribed != subscribed) + { + this->subscribed = subscribed; + if (subscribed) + { + if (subscribedByDataSignalCount == 0) + { + stream->subscribe(); + } + } + else + { + if (subscribedByDataSignalCount == 0) + { + stream->unsubscribe(); + } + } + } +} + +bool OutputDomainSignalBase::isDataSignal() +{ + return false; +} + +OutputLinearDomainSignal::OutputLinearDomainSignal(const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb) + : OutputDomainSignalBase(createSignalStream(writer, signal, tableId, logCb), signal, logCb) + , linearStream(std::dynamic_pointer_cast(stream)) +{} + +void OutputLinearDomainSignal::toStreamedSignal(const SignalPtr& signal, const SignalProps& sigProps) +{ + SignalDescriptorConverter::ToStreamedLinearSignal(signal, linearStream, sigProps); +} + +LinearTimeSignalPtr OutputLinearDomainSignal::createSignalStream( + const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb) +{ + LinearTimeSignalPtr linearStream; + + const auto signalId = signal.getGlobalId(); + auto descriptor = signal.getDescriptor(); + + // streaming-lt supports only 64bit domain values + daq::SampleType daqSampleType = descriptor.getSampleType(); + if (daqSampleType != daq::SampleType::Int64 && + daqSampleType != daq::SampleType::UInt64) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Unsupported domain signal sample type - only 64bit integer types are supported"); + + auto dataRule = descriptor.getRule(); + if (dataRule.getType() != DataRuleType::Linear) + DAQ_THROW_EXCEPTION(InvalidParameterException, "Invalid domain signal data rule - linear rule only is supported"); + + auto unit = descriptor.getUnit(); + if (!unit.assigned() || + /*unit.getId() != streaming_protocol::Unit::UNIT_ID_SECONDS ||*/ + unit.getSymbol() != "s" || + unit.getQuantity() != "time") + { + DAQ_THROW_EXCEPTION(InvalidParameterException, + "Domain signal unit parameters: {}, does not match the predefined values for linear time signal", + unit.assigned() ? unit.toString() : "not assigned"); + } + + // from streaming library side, output rate is defined as nanoseconds between two samples + const auto outputRate = dataRule.getParameters().get("delta"); + const auto resolution = + descriptor.getTickResolution().getDenominator() / descriptor.getTickResolution().getNumerator(); + + auto outputRateInNs = BaseDomainSignal::nanosecondsFromTimeTicks(outputRate, resolution); + linearStream = std::make_shared(signalId, tableId, resolution, outputRateInNs, *writer, logCb); + + SignalDescriptorConverter::ToStreamedLinearSignal(signal, linearStream, getSignalProps(signal)); + + return linearStream; +} + +BaseSynchronousSignalPtr OutputSyncValueSignal::createSignalStream( + const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb) +{ + BaseSynchronousSignalPtr syncStream; + + const auto valueDescriptor = signal.getDescriptor(); + auto sampleType = valueDescriptor.getSampleType(); + if (valueDescriptor.getPostScaling().assigned()) + sampleType = valueDescriptor.getPostScaling().getInputSampleType(); + + const auto signalId = signal.getGlobalId(); + + switch (sampleType) + { + case daq::SampleType::Int8: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::UInt8: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::Int16: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::UInt16: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::Int32: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::UInt32: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::Int64: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::UInt64: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::Float32: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::Float64: + syncStream = std::make_shared>(signalId, tableId, *writer, logCb); + break; + case daq::SampleType::ComplexFloat32: + case daq::SampleType::ComplexFloat64: + case daq::SampleType::Binary: + case daq::SampleType::Invalid: + case daq::SampleType::String: + case daq::SampleType::RangeInt64: + case daq::SampleType::Struct: + default: + DAQ_THROW_EXCEPTION(InvalidTypeException, "Unsupported data signal sample type - only real numeric types are supported"); + } + + SignalDescriptorConverter::ToStreamedValueSignal(signal, syncStream, getSignalProps(signal)); + + return syncStream; +} + +OutputSyncValueSignal::OutputSyncValueSignal(const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, OutputDomainSignalBasePtr outputDomainSignal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb) + : OutputValueSignalBase(createSignalStream(writer, signal, tableId, logCb), signal, outputDomainSignal, logCb) + , syncStream(std::dynamic_pointer_cast(stream)) +{ +} + +void OutputSyncValueSignal::writeDataPacket(const DataPacketPtr& packet) +{ + const auto domainPacket = packet.getDomainPacket(); + if (!domainPacket.assigned() || !domainPacket.getDataDescriptor().assigned()) + { + STREAMING_PROTOCOL_LOG_E("streaming-lt: cannot stream data packet without domain packet / descriptor"); + return; + } + const auto packetDomainDescriptor = domainPacket.getDataDescriptor(); + if (outputDomainSignal->isTimeConfigChanged(packetDomainDescriptor) || + isTimeConfigChanged(packetDomainDescriptor)) + { + STREAMING_PROTOCOL_LOG_E("Domain signal config mismatched, skip data packet"); + return; + } + + if (doSetStartTime) + { + uint64_t timeStamp = domainPacket.getOffset(); + auto timeValueOffset = outputDomainSignal->calcStartTimeOffset(timeStamp); + Int deltaInTicks = packetDomainDescriptor.getRule().getParameters().get("delta"); + uint64_t timeValueIndex = timeValueOffset / deltaInTicks; + syncStream->setValueIndex(timeValueIndex); + submitSignalChanges(); + + STREAMING_PROTOCOL_LOG_I("data signal {}: reset time value index: {}", daqSignal.getGlobalId(), timeValueIndex); + + doSetStartTime = false; + } + + syncStream->addData(packet.getRawData(), packet.getSampleCount()); +} + +BaseConstantSignalPtr OutputConstValueSignal::createSignalStream( + const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb) +{ + BaseConstantSignalPtr constStream; + + const auto valueDescriptor = signal.getDescriptor(); + auto sampleType = valueDescriptor.getSampleType(); + if (valueDescriptor.getPostScaling().assigned()) + sampleType = valueDescriptor.getPostScaling().getInputSampleType(); + + const auto signalId = signal.getGlobalId(); + + const auto lastValue = signal.getLastValue(); + nlohmann::json defaultStartValue(nullptr); + + switch (sampleType) + { + case daq::SampleType::Int8: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::UInt8: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::Int16: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::UInt16: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::Int32: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::UInt32: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::Int64: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::UInt64: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::Float32: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::Float64: + if (lastValue.assigned()) + defaultStartValue = static_cast(lastValue.asPtr()); + constStream = std::make_shared>(signalId, tableId, *writer, defaultStartValue, logCb); + break; + case daq::SampleType::ComplexFloat32: + case daq::SampleType::ComplexFloat64: + case daq::SampleType::Binary: + case daq::SampleType::Invalid: + case daq::SampleType::String: + case daq::SampleType::RangeInt64: + case daq::SampleType::Struct: + default: + DAQ_THROW_EXCEPTION(InvalidTypeException, "Unsupported data signal sample type - only real numeric types are supported"); + } + + SignalDescriptorConverter::ToStreamedValueSignal(signal, constStream, getSignalProps(signal)); + + return constStream; +} + +OutputConstValueSignal::OutputConstValueSignal(const daq::streaming_protocol::StreamWriterPtr& writer, + const SignalPtr& signal, OutputDomainSignalBasePtr outputDomainSignal, + const std::string& tableId, + daq::streaming_protocol::LogCallback logCb) + : OutputValueSignalBase(createSignalStream(writer, signal, tableId, logCb), signal, outputDomainSignal, logCb) + , constStream(std::dynamic_pointer_cast(stream)) +{ +} + +template +std::vector> +OutputConstValueSignal::extractConstValuesFromDataPacket(const DataPacketPtr& packet) +{ + std::vector> result; + + const auto packetData = reinterpret_cast(packet.getData()); + result.push_back({packetData[0], 0}); + + for (size_t index = 1; index < packet.getSampleCount(); ++index) + { + DataType packetDataValue = packetData[index]; + if (result.back().first != packetDataValue) + result.push_back({packetDataValue, static_cast(index)}); + } + + return result; +} + +template +void OutputConstValueSignal::writeData(const DataPacketPtr& packet, uint64_t firstValueIndex) +{ + if (doSetStartTime) + { + lastConstValue.reset(); + doSetStartTime = false; + } + + const auto values = extractConstValuesFromDataPacket(packet); + const DataType packetFirstValue = values[0].first; + + if (!lastConstValue.has_value() || std::get(lastConstValue.value()) != packetFirstValue || values.size() > 1) + { + size_t startFrom = 0; + if (lastConstValue.has_value() && std::get(lastConstValue.value()) == packetFirstValue) + startFrom = 1; + + auto valuesCount = values.size(); + + std::vector constants; + std::vector indices; + + for (size_t i = startFrom; i < valuesCount; ++i) + { + constants.push_back(static_cast(values[i].first)); + indices.push_back(values[i].second + firstValueIndex); + } + + constStream->addData(constants.data(), indices.data(), valuesCount); + } + + lastConstValue = values.back().first; +} + +void OutputConstValueSignal::writeDataPacket(const DataPacketPtr& packet) +{ + const auto domainPacket = packet.getDomainPacket(); + if (!domainPacket.assigned() || !domainPacket.getDataDescriptor().assigned()) + { + STREAMING_PROTOCOL_LOG_E("streaming-lt: cannot stream data packet without domain packet / descriptor"); + return; + } + const auto packetDomainDescriptor = domainPacket.getDataDescriptor(); + if (outputDomainSignal->isTimeConfigChanged(packetDomainDescriptor) || + isTimeConfigChanged(packetDomainDescriptor)) + { + STREAMING_PROTOCOL_LOG_E("Domain signal config mismatched, skip data packet"); + return; + } + + uint64_t timeStamp = domainPacket.getOffset(); + auto timeValueOffset = outputDomainSignal->calcStartTimeOffset(timeStamp); + Int deltaInTicks = packetDomainDescriptor.getRule().getParameters().get("delta"); + uint64_t firstValueIndex = timeValueOffset / deltaInTicks; + + auto sampleType = packet.getDataDescriptor().getSampleType(); + switch (sampleType) { + case daq::SampleType::Int8: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::Int16: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::Int32: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::Int64: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::UInt8: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::UInt16: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::UInt32: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::UInt64: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::Float32: + writeData(packet, firstValueIndex); + break; + case daq::SampleType::Float64: + writeData(packet, firstValueIndex); + break; + default: + STREAMING_PROTOCOL_LOG_E("Unsupported sample type, skip data packet"); + break; + } +} + +void OutputConstValueSignal::setSubscribed(bool subscribed) +{ + std::scoped_lock lock(subscribedSync); + + if (this->subscribed != subscribed) + { + this->subscribed = subscribed; + doSetStartTime = true; + lastConstValue.reset(); + + if (subscribed) + { + outputDomainSignal->subscribeByDataSignal(); + stream->subscribe(); + } + else + { + stream->unsubscribe(); + outputDomainSignal->unsubscribeByDataSignal(); + } + } +} + +OutputNullSignal::OutputNullSignal(const SignalPtr& signal, daq::streaming_protocol::LogCallback logCb) + : OutputSignalBase(signal, nullptr, nullptr, logCb) +{ +} + +void OutputNullSignal::writeDomainDescriptorChanges(const DataDescriptorPtr& valueDescriptor) +{ +} + +void OutputNullSignal::writeValueDescriptorChanges(const DataDescriptorPtr& domainDescriptor) +{ +} + +void OutputNullSignal::writeDaqDataPacket(const DataPacketPtr& packet) +{ +} + +void OutputNullSignal::setSubscribed(bool subscribed) +{ + std::scoped_lock lock(subscribedSync); + + this->subscribed = subscribed; +} + +bool OutputNullSignal::isDataSignal() +{ + return daqSignal.getDomainSignal().assigned(); +} + +void OutputNullSignal::toStreamedSignal(const SignalPtr& signal, const SignalProps& sigProps) +{ +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/signal_descriptor_converter.cpp b/shared/libraries/websocket_streaming/src/signal_descriptor_converter.cpp new file mode 100644 index 0000000..696bafc --- /dev/null +++ b/shared/libraries/websocket_streaming/src/signal_descriptor_converter.cpp @@ -0,0 +1,652 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "streaming_protocol/Types.h" + +#include "websocket_streaming/signal_descriptor_converter.h" + +#include +#include + +#include "streaming_protocol/BaseDomainSignal.hpp" +#include "streaming_protocol/BaseValueSignal.hpp" +#include "streaming_protocol/LinearTimeSignal.hpp" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +/** + * @todo Only scalar values are supported for now. No structs. No dimensions. + */ +SubscribedSignalInfo SignalDescriptorConverter::ToDataDescriptor( + const daq::streaming_protocol::SubscribedSignal& subscribedSignal, + const daq::ContextPtr& context) +{ + SubscribedSignalInfo sInfo; + auto dataDescriptorBuilder = DataDescriptorBuilder(); + + // *** meta "definition" start *** + // get metainfo received via signal "definition" object and stored in SubscribedSignal struct + dataDescriptorBuilder.setRule(GetRule(subscribedSignal)); + + if (subscribedSignal.isTimeSignal()) + { + uint64_t numerator = 1; + uint64_t denominator = subscribedSignal.timeBaseFrequency(); + auto resolution = Ratio(static_cast(numerator), static_cast(denominator)); + dataDescriptorBuilder.setTickResolution(resolution); + } + + daq::streaming_protocol::SampleType streamingSampleType = subscribedSignal.dataValueType(); + daq::SampleType daqSampleType = Convert(streamingSampleType); + dataDescriptorBuilder.setSampleType(daqSampleType); + + if (daqSampleType == daq::SampleType::Struct) + { + const auto& details = subscribedSignal.datatypeDetails(); + auto fields = List(); + + auto fieldNames = List(); + auto fieldTypes = List(); + + for (const auto& field : details) + { + auto fieldBuilder = DataDescriptorBuilder(); + fieldBuilder.setName(String(field.at("name"))); + fieldBuilder.setSampleType(ConvertSampleTypeString(field.value("dataType", ""))); + fieldNames.pushBack(fieldBuilder.getName()); + + if (field.count("dimensions") > 0) + { + auto daqDimensions = List(); + fieldTypes.pushBack(SimpleType(daq::CoreType::ctList)); + + auto dimensions = field["dimensions"]; + for (const auto& dimension : dimensions) + { + if (dimension.at("rule") != "linear") + DAQ_THROW_EXCEPTION(ConversionFailedException, "Struct has field with unsupported dimension"); + + daqDimensions.pushBack( + DimensionBuilder() + .setName(String(dimension.at("name"))) + .setRule(LinearDimensionRule( + static_cast(dimension.at("linear").at("delta")), + static_cast(dimension.at("linear").at("start")), + static_cast(dimension.at("linear").at("size")))) + .build()); + } + + fieldBuilder.setDimensions(daqDimensions); + } + + else + { + fieldTypes.pushBack(SimpleType(daq::CoreType::ctInt)); + } + + fields.pushBack(fieldBuilder.build()); + } + + dataDescriptorBuilder.setStructFields(fields); + + context.getTypeManager().addType( + StructType( + "CAN", // dataDescriptorBuilder.getName(), // XXX TODO + fieldNames, + fieldTypes + )); + } + + sInfo.signalName = subscribedSignal.memberName(); + + if (subscribedSignal.unitId() != daq::streaming_protocol::Unit::UNIT_ID_NONE) + { + auto unit = Unit(subscribedSignal.unitDisplayName(), + subscribedSignal.unitId(), + "", + subscribedSignal.unitQuantity()); + + dataDescriptorBuilder.setUnit(unit); + } + + dataDescriptorBuilder.setOrigin(subscribedSignal.timeBaseEpochAsString()); + + auto streamingRange = subscribedSignal.range(); + if (streamingRange.isUnlimited()) + { + // An unlimited range indicates that it is not set within the "definition" object. + // Since the range is used to configure the renderer, we need to set a default value for fusion + // (non-openDAQ) device signals. + // If the signal is owned by an openDAQ-enabled server, the value range, if present, + // is specified within the "definition" object or alternatively might be set within + // the "interpretation" object, if so that value will override the default one. + if (!subscribedSignal.isTimeSignal()) + dataDescriptorBuilder.setValueRange(CreateDefaultRange(dataDescriptorBuilder.getSampleType())); + } + else + { + dataDescriptorBuilder.setValueRange(Range(streamingRange.low, streamingRange.high)); + } + + // get linear post scaling from signal definition if it is not default - one-to-one scaling + auto streamingLinearPostScaling = subscribedSignal.postScaling(); + if (!streamingLinearPostScaling.isOneToOne()) + { + auto daqLinearPostScaling = LinearScaling(streamingLinearPostScaling.scale, + streamingLinearPostScaling.offset, + daqSampleType, + ScaledSampleType::Float64); + dataDescriptorBuilder.setPostScaling(daqLinearPostScaling); + // overwrite sample type when linear scaling is present + dataDescriptorBuilder.setSampleType(SampleType::Float64); + } + // *** meta "definition" end *** + + auto bitsInterpretation = subscribedSignal.bitsInterpretationObject(); + DecodeBitsInterpretationObject(bitsInterpretation, dataDescriptorBuilder); + + // --- meta "interpretation" start --- + // overwrite/add descriptor fields with ones from optional "interpretation" object + auto extra = subscribedSignal.interpretationObject(); + DecodeInterpretationObject(extra, dataDescriptorBuilder); + + sInfo.dataDescriptor = dataDescriptorBuilder.build(); + if (extra.count("sig_name") > 0) + sInfo.signalProps.name = extra["sig_name"]; + if (extra.count("sig_desc") > 0) + sInfo.signalProps.description = extra["sig_desc"]; + // --- meta "interpretation" end --- + + return sInfo; +} + +void SignalDescriptorConverter::ToStreamedValueSignal(const daq::SignalPtr& valueSignal, + daq::streaming_protocol::BaseValueSignalPtr valueStream, + const SignalProps& sigProps) +{ + auto dataDescriptor = valueSignal.getDescriptor(); + if (!dataDescriptor.assigned()) + return; + + // *** meta "definition" start *** + // set/verify fields which will be lately encoded into signal "definition" object + valueStream->setMemberName(valueSignal.getName()); + + // Data type of stream can not be changed. Complain upon change! + daq::SampleType daqSampleType = dataDescriptor.getSampleType(); + if (dataDescriptor.getPostScaling().assigned()) + daqSampleType = dataDescriptor.getPostScaling().getInputSampleType(); + + daq::streaming_protocol::SampleType requestedSampleType = Convert(daqSampleType); + if (requestedSampleType != valueStream->getSampleType()) + DAQ_THROW_EXCEPTION(ConversionFailedException, "Sample type has been changed"); + + UnitPtr unit = dataDescriptor.getUnit(); + if (unit.assigned()) + valueStream->setUnit(unit.getId(), unit.getSymbol()); + + if (dataDescriptor.getValueRange().assigned()) + { + auto daqRange = dataDescriptor.getValueRange(); + daq::streaming_protocol::Range streamingRange; + streamingRange.high = daqRange.getHighValue().getFloatValue(); + streamingRange.low = daqRange.getLowValue().getFloatValue(); + valueStream->setRange(streamingRange); + } + + auto daqPostScaling = dataDescriptor.getPostScaling(); + if (daqPostScaling.assigned() && daqPostScaling.getType() == daq::ScalingType::Linear) + { + daq::streaming_protocol::PostScaling streamingLinearPostScaling; + streamingLinearPostScaling.scale = daqPostScaling.getParameters().get("scale"); + streamingLinearPostScaling.offset = daqPostScaling.getParameters().get("offset"); + valueStream->setPostScaling(streamingLinearPostScaling); + } + // *** meta "definition" end *** + + // --- meta "interpretation" start --- + nlohmann::json extra; + EncodeInterpretationObject(dataDescriptor, extra); + + if (sigProps.name.has_value()) + extra["sig_name"] = sigProps.name.value(); + if (sigProps.description.has_value()) + extra["sig_desc"] = sigProps.description.value(); + + valueStream->setInterpretationObject(extra); + // --- meta "interpretation" end --- +} + +void SignalDescriptorConverter::ToStreamedLinearSignal(const daq::SignalPtr& domainSignal, + streaming_protocol::LinearTimeSignalPtr linearStream, + const SignalProps& sigProps) +{ + auto domainDescriptor = domainSignal.getDescriptor(); + if (!domainDescriptor.assigned()) + return; + + // *** meta "definition" start *** + + // streaming-lt supports only 64bit domain values + daq::SampleType daqSampleType = domainDescriptor.getSampleType(); + daq::streaming_protocol::SampleType requestedSampleType = Convert(daqSampleType); + if (requestedSampleType != daq::streaming_protocol::SampleType::SAMPLETYPE_S64 && + requestedSampleType != daq::streaming_protocol::SampleType::SAMPLETYPE_U64) + DAQ_THROW_EXCEPTION(ConversionFailedException, "Non-64bit domain sample types are not supported"); + + DataRulePtr rule = domainDescriptor.getRule(); + SetLinearTimeRule(rule, linearStream); + + auto resolution = domainDescriptor.getTickResolution(); + linearStream->setTimeTicksPerSecond(resolution.getDenominator() / resolution.getNumerator()); + // *** meta "definition" end *** + + // --- meta "interpretation" start --- + + // openDAQ does not encode meta "definition" object directly for domain signal + // domain signal "definition" is hardcoded on library level + // so openDAQ uses "interpretation" object to transmit metadata which describes domain signal + nlohmann::json extra; + if (domainDescriptor.assigned()) + EncodeInterpretationObject(domainDescriptor, extra); + + if (sigProps.name.has_value()) + extra["sig_name"] = sigProps.name.value(); + if (sigProps.description.has_value()) + extra["sig_desc"] = sigProps.description.value(); + + linearStream->setInterpretationObject(extra); + // --- meta "interpretation" end --- +} + +/** + * @throws ConversionFailedException + */ +daq::DataRulePtr SignalDescriptorConverter::GetRule(const daq::streaming_protocol::SubscribedSignal& subscribedSignal) +{ + switch (subscribedSignal.ruleType()) + { + case daq::streaming_protocol::RULETYPE_CONSTANT: + { + return ConstantDataRule(); + } + break; + case daq::streaming_protocol::RULETYPE_EXPLICIT: + { + return ExplicitDataRule(); + } + break; + case daq::streaming_protocol::RULETYPE_LINEAR: + { + nlohmann::json linearDeltaJson = subscribedSignal.linearDeltaMeta(); + if (linearDeltaJson.is_number_integer()) + return LinearDataRule(linearDeltaJson.get(), 0); + else + return LinearDataRule(linearDeltaJson.get(), 0); + } + break; + default: + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unsupported data rule type"); + } +} + +/** + * @throws ConversionFailedException + */ +void SignalDescriptorConverter::SetLinearTimeRule(const daq::DataRulePtr& rule, daq::streaming_protocol::LinearTimeSignalPtr linearStream) +{ + if (!rule.assigned() || rule.getType() != DataRuleType::Linear) + { + DAQ_THROW_EXCEPTION(ConversionFailedException, "Time rule is not supported"); + } + uint64_t delta = rule.getParameters().get("delta"); + linearStream->setOutputRate(delta); +} + +/** + * @throws ConversionFailedException + */ +daq::SampleType SignalDescriptorConverter::Convert(daq::streaming_protocol::SampleType dataType) +{ + switch (dataType) + { + case daq::streaming_protocol::SampleType::SAMPLETYPE_S8: + return daq::SampleType::Int8; + case daq::streaming_protocol::SampleType::SAMPLETYPE_S16: + return daq::SampleType::Int16; + case daq::streaming_protocol::SampleType::SAMPLETYPE_S32: + return daq::SampleType::Int32; + case daq::streaming_protocol::SampleType::SAMPLETYPE_S64: + return daq::SampleType::Int64; + case daq::streaming_protocol::SampleType::SAMPLETYPE_U8: + return daq::SampleType::UInt8; + case daq::streaming_protocol::SampleType::SAMPLETYPE_U16: + return daq::SampleType::UInt16; + case daq::streaming_protocol::SampleType::SAMPLETYPE_U32: + return daq::SampleType::UInt32; + case daq::streaming_protocol::SampleType::SAMPLETYPE_U64: + return daq::SampleType::UInt64; + case daq::streaming_protocol::SampleType::SAMPLETYPE_COMPLEX32: + return daq::SampleType::ComplexFloat32; + case daq::streaming_protocol::SampleType::SAMPLETYPE_COMPLEX64: + return daq::SampleType::ComplexFloat64; + case daq::streaming_protocol::SampleType::SAMPLETYPE_REAL32: + return daq::SampleType::Float32; + case daq::streaming_protocol::SampleType::SAMPLETYPE_REAL64: + return daq::SampleType::Float64; + case daq::streaming_protocol::SampleType::SAMPLETYPE_BITFIELD32: + return daq::SampleType::UInt32; + case daq::streaming_protocol::SampleType::SAMPLETYPE_BITFIELD64: + return daq::SampleType::UInt64; + case daq::streaming_protocol::SampleType::SAMPLETYPE_STRUCT: + return daq::SampleType::Struct; + default: + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unsupported input sample type"); + } +} + +/** + * @throws ConversionFailedException + */ +daq::SampleType SignalDescriptorConverter::ConvertSampleTypeString(const std::string& sampleType) +{ + if (sampleType == "uint8") + return daq::SampleType::UInt8; + if (sampleType == "uint16") + return daq::SampleType::UInt16; + if (sampleType == "uint32") + return daq::SampleType::UInt32; + if (sampleType == "uint64") + return daq::SampleType::UInt64; + if (sampleType == "int8") + return daq::SampleType::Int8; + if (sampleType == "int16") + return daq::SampleType::Int16; + if (sampleType == "int32") + return daq::SampleType::Int32; + if (sampleType == "int64") + return daq::SampleType::Int64; + if (sampleType == "real32") + return daq::SampleType::Float32; + if (sampleType == "real64") + return daq::SampleType::Float64; + + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unsupported input sample type"); +} + +/** + * @throws ConversionFailedException + */ +daq::streaming_protocol::SampleType SignalDescriptorConverter::Convert(daq::SampleType sampleType) +{ + switch (sampleType) + { + case daq::SampleType::Int8: + return daq::streaming_protocol::SampleType::SAMPLETYPE_S8; + break; + case daq::SampleType::Int16: + return daq::streaming_protocol::SampleType::SAMPLETYPE_S16; + break; + case daq::SampleType::Int32: + return daq::streaming_protocol::SampleType::SAMPLETYPE_S32; + break; + case daq::SampleType::Int64: + return daq::streaming_protocol::SampleType::SAMPLETYPE_S64; + break; + case daq::SampleType::ComplexFloat32: + return daq::streaming_protocol::SampleType::SAMPLETYPE_COMPLEX32; + break; + case daq::SampleType::ComplexFloat64: + return daq::streaming_protocol::SampleType::SAMPLETYPE_COMPLEX64; + break; + case daq::SampleType::Float32: + return daq::streaming_protocol::SampleType::SAMPLETYPE_REAL32; + break; + case daq::SampleType::Float64: + return daq::streaming_protocol::SampleType::SAMPLETYPE_REAL64; + break; + case daq::SampleType::UInt8: + return daq::streaming_protocol::SampleType::SAMPLETYPE_U8; + break; + case daq::SampleType::UInt16: + return daq::streaming_protocol::SampleType::SAMPLETYPE_U16; + break; + case daq::SampleType::UInt32: + return daq::streaming_protocol::SampleType::SAMPLETYPE_U32; + break; + case daq::SampleType::UInt64: + return daq::streaming_protocol::SampleType::SAMPLETYPE_U64; + break; + case daq::SampleType::Binary: + case daq::SampleType::Struct: + case daq::SampleType::Invalid: + case daq::SampleType::String: + case daq::SampleType::RangeInt64: + default: + DAQ_THROW_EXCEPTION(ConversionFailedException, "Unsupported output sample type"); + } +} + +daq::RangePtr SignalDescriptorConverter::CreateDefaultRange(daq::SampleType sampleType) +{ + switch (sampleType) + { + case daq::SampleType::UInt8: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::Int8: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::UInt16: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::Int16: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::UInt32: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::Int32: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::UInt64: + // range integer values are of signed type so the highest value is max of signed type + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::Int64: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::Float32: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + case daq::SampleType::Float64: + return Range(std::numeric_limits::lowest(), std::numeric_limits::max()); + break; + default: + return nullptr; + break; + } +} + +void SignalDescriptorConverter::EncodeInterpretationObject(const DataDescriptorPtr& dataDescriptor, nlohmann::json& extra) +{ + // put descriptor name into interpretation object + if (dataDescriptor.getName().assigned()) + extra["desc_name"] = dataDescriptor.getName(); + + if (dataDescriptor.getMetadata().assigned()) + { + auto meta = extra["metadata"]; + for (const auto& [key, value] : dataDescriptor.getMetadata()) + meta[key.getCharPtr()] = value; + extra["metadata"] = meta; + } + + if (dataDescriptor.getUnit().assigned()) + { + auto unit1 = dataDescriptor.getUnit(); + extra["unit"]["id"] = unit1.getId(); + extra["unit"]["name"] = unit1.getName(); + extra["unit"]["symbol"] = unit1.getSymbol(); + extra["unit"]["quantity"] = unit1.getQuantity(); + } + + if (dataDescriptor.getValueRange().assigned()) + { + auto range = dataDescriptor.getValueRange(); + extra["range"]["low"] = range.getLowValue(); + extra["range"]["high"] = range.getHighValue(); + } + + if (dataDescriptor.getOrigin().assigned()) + extra["origin"] = dataDescriptor.getOrigin(); + + if (dataDescriptor.getRule().assigned()) + { + auto rule = dataDescriptor.getRule(); + extra["rule"]["type"] = rule.getType(); + extra["rule"]["parameters"] = DictToJson(rule.getParameters()); + } + + if (dataDescriptor.getPostScaling().assigned()) + { + auto scaling = dataDescriptor.getPostScaling(); + extra["scaling"]["inputType"] = scaling.getInputSampleType(); + extra["scaling"]["outputType"] = scaling.getOutputSampleType(); + extra["scaling"]["scalingType"] = scaling.getType(); + extra["scaling"]["parameters"] = DictToJson(scaling.getParameters()); + } +} + +void SignalDescriptorConverter::DecodeBitsInterpretationObject(const nlohmann::json& bits, DataDescriptorBuilderPtr& dataDescriptorBuilder) +{ + if (!bits.empty() && bits.is_array()) + { + auto metadata = dataDescriptorBuilder.getMetadata(); + metadata.set("bits", bits.dump()); + } +} + +void SignalDescriptorConverter::DecodeInterpretationObject(const nlohmann::json& extra, DataDescriptorBuilderPtr& dataDescriptorBuilder) +{ + // sets descriptor name when corresponding field is present in interpretation object + if (extra.contains("desc_name")) + dataDescriptorBuilder.setName(String(extra["desc_name"])); + + if (extra.contains("metadata")) + { + auto meta = JsonToObject(extra["metadata"]); + dataDescriptorBuilder.setMetadata(meta); + } + + if (extra.contains("unit")) + { + auto unitObj = extra["unit"]; + auto unit = Unit(String(unitObj["symbol"]), Integer(unitObj["id"]), String(unitObj["name"]), String(unitObj["quantity"])); + dataDescriptorBuilder.setUnit(unit); + } + + if (extra.contains("range")) + { + auto rangeObj = extra["range"]; + auto low = std::stoi(std::string(rangeObj["low"])); + auto high = std::stoi(std::string(rangeObj["high"])); + auto range = Range(low, high); + dataDescriptorBuilder.setValueRange(range); + } + + if (extra.contains("origin")) + dataDescriptorBuilder.setOrigin(String(extra["origin"])); + + if (extra.contains("rule") && extra["rule"].contains("parameters") && extra["rule"]["parameters"].is_object()) + { + auto params = JsonToObject(extra["rule"]["parameters"]); + params.freeze(); + + auto rule = DataRuleBuilder().setType(extra["rule"]["type"]).setParameters(params).build(); + dataDescriptorBuilder.setRule(rule); + } + + if (extra.contains("scaling") && extra["scaling"].contains("parameters") && extra["scaling"]["parameters"].is_object()) + { + auto params = JsonToObject(extra["scaling"]["parameters"]); + params.freeze(); + + auto scaling = ScalingBuilder() + .setInputDataType(extra["scaling"]["inputType"]) + .setOutputDataType(extra["scaling"]["outputType"]) + .setScalingType(extra["scaling"]["scalingType"]) + .setParameters(params) + .build(); + dataDescriptorBuilder.setPostScaling(scaling); + + // overwrite sample type when scaling is present + dataDescriptorBuilder.setSampleType(convertScaledToSampleType(scaling.getOutputSampleType())); + } +} + +nlohmann::json SignalDescriptorConverter::DictToJson(const DictPtr& dict) +{ + nlohmann::json json; + + for (const auto& [key, value] : dict) + { + if (value.supportsInterface()) + json[key.getCharPtr()] = value.asPtr().toVector(); + else if (value.supportsInterface()) + json[key.getCharPtr()] = DictToJson(value); + else if (value.supportsInterface()) + json[key.getCharPtr()] = (Float) value; + else if (value.supportsInterface()) + json[key.getCharPtr()] = (Int) value; + else + json[key.getCharPtr()] = value; + } + + return json; +} + +ObjectPtr SignalDescriptorConverter::JsonToObject(const nlohmann::json& json) +{ + if (json.is_object()) + { + auto dict = Dict(); + for (const auto& entry : json.items()) + if (auto obj = JsonToObject(entry.value()); obj.assigned()) + dict[entry.key()] = obj; + return dict; + } + else if (json.is_array()) + { + auto list = List(); + for (const auto& entry : json) + if (auto obj = JsonToObject(entry); obj.assigned()) + list.pushBack(obj); + return list; + } + else if (json.is_number_float()) + return json.get(); + + else if (json.is_number_integer()) + return json.get(); + + else if (json.is_string()) + return StringPtr(json.get()); + + else if (json.is_boolean()) + return Boolean(json.get()); + + else + return nullptr; +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/streaming_client.cpp b/shared/libraries/websocket_streaming/src/streaming_client.cpp new file mode 100644 index 0000000..d8573c9 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/streaming_client.cpp @@ -0,0 +1,787 @@ +#include "websocket_streaming/streaming_client.h" +#include +#include +#include +#include +#include "stream/WebsocketClientStream.hpp" +#include "stream/TcpClientStream.hpp" +#include "streaming_protocol/SignalContainer.hpp" +#include "opendaq/custom_log.h" +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +using namespace daq::stream; +using namespace daq::streaming_protocol; + +// parsing connection string to four groups: prefix, host, port, path +static const std::regex RegexIpv6Hostname(R"(^(.+://)?(?:\[([a-fA-F0-9:]+(?:\%[a-zA-Z0-9_\.-~]+)?)\])(?::(\d+))?(/.*)?$)"); +static const std::regex RegexIpv4Hostname(R"(^(.+://)?([^:/\s]+)(?::(\d+))?(/.*)?$)"); + +StreamingClient::StreamingClient(const ContextPtr& context, const std::string& connectionString, bool useRawTcpConnection) + : context(context) + , logger(context.getLogger()) + , loggerComponent( logger.assigned() ? logger.getOrAddComponent("StreamingClient") : throw ArgumentNullException("Logger must not be null") ) + , logCallback( [this](spdlog::source_loc location, spdlog::level::level_enum level, const char* msg) { + this->loggerComponent.logMessage(SourceLocation{location.filename, location.line, location.funcname}, msg, static_cast(level)); + }) + , signalContainer(logCallback) + , useRawTcpConnection(useRawTcpConnection) +{ + parseConnectionString(connectionString); + startBackgroundContext(); +} + +StreamingClient::StreamingClient(const ContextPtr& context, const std::string& host, uint16_t port, const std::string& target, bool useRawTcpConnection) + : logger(context.getLogger()) + , loggerComponent( logger.assigned() ? logger.getOrAddComponent("StreamingClient") : throw ArgumentNullException("Logger must not be null") ) + , logCallback( [this](spdlog::source_loc location, spdlog::level::level_enum level, const char* msg) { + this->loggerComponent.logMessage(SourceLocation{location.filename, location.line, location.funcname}, msg, static_cast(level)); + }) + , host(host) + , port(port) + , target(target) + , signalContainer(logCallback) + , useRawTcpConnection(useRawTcpConnection) +{ + startBackgroundContext(); +} + +void daq::websocket_streaming::StreamingClient::startBackgroundContext() +{ + clientBackgroundThread = std::thread( + [this]() + { + using namespace boost::asio; + executor_work_guard workGuard(backgroundContext.get_executor()); + backgroundContext.run(); + } + ); +} + +void StreamingClient::stopBackgroundContext() +{ + backgroundContext.stop(); + if (clientBackgroundThread.get_id() != std::this_thread::get_id()) + { + if (clientBackgroundThread.joinable()) + { + clientBackgroundThread.join(); + LOG_I("Websocket streaming client background thread joined"); + } + else + { + LOG_W("Websocket streaming client background thread is not joinable"); + } + } + else + { + LOG_C("Websocket streaming client background thread cannot join itself"); + } +} + +StreamingClient::~StreamingClient() +{ + disconnect(); + stopBackgroundContext(); +} + +bool StreamingClient::connect() +{ + if (connected) + return true; + if (host.empty() || port == 0) + return false; + + connected = false; + + auto signalMetaCallback = [this](const SubscribedSignal& subscribedSignal, const std::string& method, const nlohmann::json& params) + { onSignalMeta(subscribedSignal, method, params); }; + + auto protocolMetaCallback = [this](ProtocolHandler& protocolHandler, const std::string& method, const nlohmann::json& params) + { onProtocolMeta(protocolHandler, method, params); }; + + auto messageCallback = [this](const SubscribedSignal& subscribedSignal, uint64_t timeStamp, const uint8_t* data, size_t valueCount) + { onMessage(subscribedSignal, timeStamp, data, valueCount); }; + + signalContainer.setSignalMetaCb(signalMetaCallback); + signalContainer.setDataAsValueCb(messageCallback); + + std::unique_ptr clientStream; + if (useRawTcpConnection) + clientStream = std::make_unique(ioContext, host, std::to_string(port)); + else + clientStream = std::make_unique(ioContext, host, std::to_string(port), target); + + protocolHandler = std::make_shared(ioContext, signalContainer, protocolMetaCallback, logCallback); + + std::unique_lock lock(clientMutex); + protocolHandler->startWithSyncInit(std::move(clientStream)); + + ioContext.restart(); + clientIoThread = std::thread([this]() { ioContext.run(); }); + + conditionVariable.wait_for(lock, connectTimeout, [this]() { return connected; }); + + if (connected) + checkTmpSubscribedSignalsInit(); + + return connected; +} + +void StreamingClient::disconnect() +{ + ioContext.stop(); + if (clientIoThread.get_id() != std::this_thread::get_id()) + { + if (clientIoThread.joinable()) + { + clientIoThread.join(); + LOG_I("Websocket streaming client IO thread joined"); + } + else + { + LOG_W("Websocket streaming client IO thread is not joinable"); + } + } + else + { + LOG_C("Websocket streaming client IO thread cannot join itself"); + } + + connected = false; +} + +void StreamingClient::onPacket(const OnPacketCallback& callack) +{ + onPacketCallback = callack; +} + +void StreamingClient::onDeviceAvailableSignalInit(const OnSignalCallback& callback) +{ + onAvailableSignalInitCb = callback; +} + +void StreamingClient::onDeviceSignalUpdated(const OnSignalCallback& callback) +{ + onSignalUpdatedCallback = callback; +} + +void StreamingClient::onDeviceDomainSingalInit(const OnDomainSignalInitCallback& callback) +{ + onDomainSignalInitCallback = callback; +} + +void StreamingClient::onStreamingAvailableSignals(const OnAvailableSignalsCallback& callback) +{ + onAvailableStreamingSignalsCb = callback; +} + +void StreamingClient::onDeviceAvailableSignals(const OnAvailableSignalsCallback& callback) +{ + onAvailableDeviceSignalsCb = callback; +} + +void StreamingClient::onStreamingUnavailableSignals(const OnAvailableSignalsCallback& callback) +{ + onUnavailableStreamingSignalsCb = callback; +} + +void StreamingClient::onDeviceUnavailableSignals(const OnAvailableSignalsCallback& callback) +{ + onUnavailableDeviceSignalsCb = callback; +} + +void StreamingClient::onStreamingHiddenSignal(const OnSignalCallback& callback) +{ + onHiddenStreamingSignalCb = callback; +} + +void StreamingClient::onDeviceHiddenSignal(const OnSignalCallback& callback) +{ + onHiddenDeviceSignalInitCb = callback; +} + +void StreamingClient::onDeviceSignalsInitDone(const OnSignalsInitDoneCallback& callback) +{ + onSignalsInitDone = callback; +} + +void StreamingClient::onSubscriptionAck(const OnSubsciptionAckCallback& callback) +{ + onSubscriptionAckCallback = callback; +} + +std::string StreamingClient::getHost() +{ + return host; +} + +uint16_t StreamingClient::getPort() +{ + return port; +} + +std::string StreamingClient::getTarget() +{ + return target; +} + +bool StreamingClient::isConnected() +{ + return connected; +} + +void StreamingClient::setConnectTimeout(std::chrono::milliseconds timeout) +{ + this->connectTimeout = timeout; +} + +void StreamingClient::parseConnectionString(const std::string& url) +{ + // this is not great but it is convenient until we have a way to pass configuration parameters to a client device + + host = ""; + port = 7414; + target = "/"; + + std::smatch match; + + bool parsed = false; + parsed = std::regex_search(url, match, RegexIpv6Hostname); + if (!parsed) + parsed = std::regex_search(url, match, RegexIpv4Hostname); + + if (parsed) + { + host = match[2]; + if (match[3].matched) + port = std::stoi(match[3]); + if (match[4].matched) + target = match[4]; + } +} + +void StreamingClient::onSignalMeta(const SubscribedSignal& subscribedSignal, const std::string& method, const nlohmann::json& params) +{ + if (method == daq::streaming_protocol::META_METHOD_SIGNAL) + onSignal(subscribedSignal, params); + + std::string signalId = subscribedSignal.signalId(); + + // triggers ack only for available signals, but not for hidden ones + if (method == daq::streaming_protocol::META_METHOD_SUBSCRIBE) + { + if (auto it = availableSignals.find(signalId); it != availableSignals.end()) + { + auto inputSignal = it->second; + // forwards ACK to upper level only if signal is explicitly subscribed + // and doesn't if just temporary subscribed for descriptors initialization + // i.e. skips the first subscribe ACK from server if signal is not explicitly subscribed by streaming + if (inputSignal && inputSignal->getSubscribed()) + { + // ignores ACKs for domain signals + if (!inputSignal->isDomainSignal()) + onSubscriptionAckCallback(subscribedSignal.signalId(), true); + } + } + } + else if (method == daq::streaming_protocol::META_METHOD_UNSUBSCRIBE) + { + if (auto it = availableSigInitStatus.find(signalId); it != availableSigInitStatus.end()) + { + // skips the first unsubscribe ACK from server + if (std::get<2>(it->second)) + { + // ignores ACKs for domain signals + if (!subscribedSignal.isTimeSignal()) + onSubscriptionAckCallback(subscribedSignal.signalId(), false); + } + else + { + std::get<2>(it->second) = true; + } + } + } +} + +void StreamingClient::onProtocolMeta(daq::streaming_protocol::ProtocolHandler& protocolHandler, + const std::string& method, + const nlohmann::json& params) +{ + if (method == daq::streaming_protocol::META_METHOD_AVAILABLE) + { + auto availableSignalsArray = params.find(META_SIGNALIDS); + if (availableSignalsArray != params.end() && availableSignalsArray->is_array()) + { + availableSignalsHandler(availableSignalsArray); + } + } + else if (method == daq::streaming_protocol::META_METHOD_UNAVAILABLE) + { + auto unavailableSignalsArray = params.find(META_SIGNALIDS); + if (unavailableSignalsArray != params.end() && unavailableSignalsArray->is_array()) + { + unavailableSignalsHandler(unavailableSignalsArray); + } + } +} + +void StreamingClient::availableSignalsHandler(const nlohmann::json::const_iterator& availableSignalsArray) +{ + std::vector signalIds; + for (const auto& arrayItem : *availableSignalsArray) + { + std::string signalId = arrayItem; + signalIds.push_back(signalId); + + if (auto signalIt = availableSignals.find(signalId); signalIt == availableSignals.end()) + { + availableSignals.insert({signalId, InputPlaceHolderSignal(signalId, logCallback)}); + } + else + { + LOG_E("Received duplicate of available signal. ID is {}.", signalId); + } + } + + onAvailableDeviceSignalsCb(signalIds); + onAvailableStreamingSignalsCb(signalIds); + + std::unique_lock lock(clientMutex); + + for (const auto& signalId : signalIds) + { + tmpSubscribedSignalIds.insert(signalId); + + std::promise signalInitPromise; + std::future signalInitFuture = signalInitPromise.get_future(); + availableSigInitStatus.insert_or_assign( + signalId, + std::make_tuple( + std::move(signalInitPromise), + std::move(signalInitFuture), + false + ) + ); + } + + // signal meta-information (signal description, tableId, related signals, etc.) + // is published only for subscribed signals. + // as workaround we temporarily subscribe all signals to receive signal meta-info + // and initialize signal descriptors + this->protocolHandler->subscribe(signalIds); + + if (connected) + { + // wait for signals initialization done in a separate thread + backgroundContext.dispatch( + [this]() + { + std::unique_lock lock(clientMutex); + checkTmpSubscribedSignalsInit(); + } + ); + } + else + { + connected = true; + conditionVariable.notify_all(); + } +} + +void StreamingClient::unavailableSignalsHandler(const nlohmann::json::const_iterator& unavailableSignalsArray) +{ + std::vector signalIds; + for (const auto& arrayItem : *unavailableSignalsArray) + { + std::string signalId = arrayItem; + + if (auto signalIt = availableSignals.find(signalId); signalIt != availableSignals.end()) + { + availableSignals.erase(signalIt); + } + else + { + LOG_E("Received unavailable signal which were not available before. ID is {}.", signalId); + } + + signalIds.push_back(signalId); + } + + onUnavailableStreamingSignalsCb(signalIds); + onUnavailableDeviceSignalsCb(signalIds); +} + +void StreamingClient::subscribeSignal(const std::string& signalId) +{ + if (auto it = availableSignals.find(signalId); it != availableSignals.end()) + { + if (auto inputSignal = it->second) + inputSignal->setSubscribed(true); + protocolHandler->subscribe({signalId}); + } +} + +void StreamingClient::unsubscribeSignal(const std::string& signalId) +{ + if (auto it = availableSignals.find(signalId); it != availableSignals.end()) + { + if (auto inputSignal = it->second) + inputSignal->setSubscribed(false); + protocolHandler->unsubscribe({signalId}); + } +} + +void StreamingClient::onMessage(const daq::streaming_protocol::SubscribedSignal& subscribedSignal, + uint64_t timeStamp, + const uint8_t* data, + size_t valueCount) +{ + std::string id = subscribedSignal.signalId(); + std::size_t dataSize = subscribedSignal.dataValueSize() * valueCount; + + NumberPtr domainValue = static_cast(timeStamp); + InputSignalBasePtr inputSignal = nullptr; + + if (auto availableSigIt = availableSignals.find(id); availableSigIt != availableSignals.end()) + inputSignal = availableSigIt->second; + else if (auto hiddenSigIt = hiddenSignals.find(id); hiddenSigIt != hiddenSignals.end()) + inputSignal = hiddenSigIt->second; + + if (inputSignal && + !isPlaceHolderSignal(inputSignal) && + inputSignal->hasDescriptors()) + { + if (inputSignal->isCountable()) + { + DataPacketPtr domainPacket; + if (inputSignal->isDomainSignal()) + { + domainPacket = inputSignal->generateDataPacket(domainValue, data, dataSize, valueCount, nullptr); + inputSignal->setLastPacket(domainPacket); + if (domainPacket.assigned()) + onPacketCallback(id, domainPacket); + } + else + { + // If the domain signal is linear-rule, we artificially generate a packet here + // using the timestamp reported by streaming-protocol-lt. If the domain signal + // is explicit-rule, we must (by requirement) already have received and cached + // the domain packet above, and we use that instead. + DataRuleType domainRuleType = DataRuleType::Other; + auto domainSignal = inputSignal->getInputDomainSignal(); + if (domainSignal) + if (auto domainDescriptor = domainSignal->getSignalDescriptor(); domainDescriptor.assigned()) + if (auto domainRule = domainDescriptor.getRule(); domainRule.assigned()) + domainRuleType = domainRule.getType(); + if (domainRuleType == DataRuleType::Explicit) + { + domainPacket = inputSignal->getInputDomainSignal()->getLastPacket(); + } + else + { + domainPacket = + inputSignal->getInputDomainSignal()->generateDataPacket(domainValue, data, dataSize, valueCount, nullptr); + if (domainPacket.assigned()) + onPacketCallback(inputSignal->getInputDomainSignal()->getSignalId(), domainPacket); + } + + auto packet = inputSignal->generateDataPacket(domainValue, data, dataSize, valueCount, domainPacket); + inputSignal->setLastPacket(packet); + if (packet.assigned()) + onPacketCallback(id, packet); + } + + auto relatedDataSignals = findDataSignalsByTableId(inputSignal->getTableId()); + + // trigger packet generation for each related signal which is not countable (is implicit) + for (auto& relatedSignal : relatedDataSignals) + { + if (!relatedSignal->isCountable()) + { + auto packet = relatedSignal->generateDataPacket(domainValue, nullptr, 0, valueCount, domainPacket); + if (packet.assigned() && relatedSignal->getSubscribed()) + onPacketCallback(relatedSignal->getSignalId(), packet); + } + } + } + else + { + inputSignal->processSamples(domainValue, data, valueCount); + } + } +} + +void StreamingClient::setDataSignal( + const daq::streaming_protocol::SubscribedSignal& subscribedSignal, + const daq::ContextPtr& context) +{ + auto sInfo = SignalDescriptorConverter::ToDataDescriptor(subscribedSignal, context); + const auto signalId = subscribedSignal.signalId(); + const auto tableId = subscribedSignal.tableId(); + bool available = false; + + auto domainInputSignal = findTimeSignalByTableId(tableId); + if (!domainInputSignal) + DAQ_THROW_EXCEPTION(NotFoundException, "Unknown domain signal for data signal {}, table {}", signalId, tableId); + + InputSignalBasePtr inputSignal = nullptr; + if (auto availableSigIt = availableSignals.find(signalId); availableSigIt != availableSignals.end()) + { + inputSignal = availableSigIt->second; + available = true; + } + else if (auto hiddenSigIt = hiddenSignals.find(signalId); hiddenSigIt != hiddenSignals.end()) + { + inputSignal = hiddenSigIt->second; + available = false; + } + + if (!inputSignal && !available) + { + inputSignal = InputSignal(signalId, tableId, sInfo, false, domainInputSignal, logCallback, subscribedSignal.constRuleStartMeta()); + hiddenSignals.insert({signalId, inputSignal}); + onHiddenStreamingSignalCb(signalId, sInfo); + onHiddenDeviceSignalInitCb(signalId, sInfo); + onDomainSignalInitCallback(signalId, domainInputSignal->getSignalId()); + publishSignalChanges(inputSignal, true, true); + } + else if (available && isPlaceHolderSignal(inputSignal)) + { + bool subscribed = inputSignal->getSubscribed(); + inputSignal = InputSignal(signalId, tableId, sInfo, false, domainInputSignal, logCallback, subscribedSignal.constRuleStartMeta()); + inputSignal->setSubscribed(subscribed); + availableSignals[signalId] = inputSignal; + onAvailableSignalInitCb(signalId, sInfo); + onDomainSignalInitCallback(signalId, domainInputSignal->getSignalId()); + setSignalInitSatisfied(signalId); + publishSignalChanges(inputSignal, true, true); + } + else + { + if (sInfo.dataDescriptor != inputSignal->getSignalDescriptor()) + { + inputSignal->setDataDescriptor(sInfo.dataDescriptor); + publishSignalChanges(inputSignal, true, false); + } + onSignalUpdatedCallback(signalId, sInfo); + } +} + +void StreamingClient::setTimeSignal( + const daq::streaming_protocol::SubscribedSignal& subscribedSignal, + const daq::ContextPtr& context) +{ + auto sInfo = SignalDescriptorConverter::ToDataDescriptor(subscribedSignal, context); + const std::string tableId = subscribedSignal.tableId(); + const std::string timeSignalId = subscribedSignal.signalId(); + bool available = false; + + InputSignalBasePtr inputSignal = nullptr; + if (auto availableSigIt = availableSignals.find(timeSignalId); availableSigIt != availableSignals.end()) + { + inputSignal = availableSigIt->second; + available = true; + } + else if (auto hiddenSigIt = hiddenSignals.find(timeSignalId); hiddenSigIt != hiddenSignals.end()) + { + inputSignal = hiddenSigIt->second; + available = false; + } + + if (!inputSignal && !available) + { + // the time signal was not published as available by server, add as hidden + inputSignal = InputSignal(timeSignalId, tableId, sInfo, true, nullptr, logCallback, subscribedSignal.constRuleStartMeta()); + hiddenSignals.insert({timeSignalId, inputSignal}); + onHiddenStreamingSignalCb(timeSignalId, sInfo); + onHiddenDeviceSignalInitCb(timeSignalId, sInfo); + } + else if (available && isPlaceHolderSignal(inputSignal)) + { + bool subscribed = inputSignal->getSubscribed(); + inputSignal = InputSignal(timeSignalId, tableId, sInfo, true, nullptr, logCallback, subscribedSignal.constRuleStartMeta()); + inputSignal->setSubscribed(subscribed); + availableSignals[timeSignalId] = inputSignal; + // the time signal is published as available by server, + // so do the initialization of its mirrored copy + onAvailableSignalInitCb(timeSignalId, sInfo); + setSignalInitSatisfied(timeSignalId); + } + else + { + if (sInfo.dataDescriptor != inputSignal->getSignalDescriptor()) + { + inputSignal->setDataDescriptor(sInfo.dataDescriptor); + + // publish new domain descriptor for all input data signals with known tableId + for (const auto& inputDataSignal : findDataSignalsByTableId(tableId)) + { + publishSignalChanges(inputDataSignal, false, true); + } + } + if (auto constRuleSignal = std::dynamic_pointer_cast(inputSignal)) + { + constRuleSignal->updateStartValue(subscribedSignal.constRuleStartMeta()); + } + onSignalUpdatedCallback(timeSignalId, sInfo); + } +} + +void StreamingClient::publishSignalChanges(const InputSignalBasePtr& signal, bool valueChanged, bool domainChanged) +{ + // signal meta information is always received by pairs of META_METHOD_SIGNAL: + // one is meta for data signal, another is meta for time signal. + // we generate event packet only after both meta are received + // and all signal descriptors are assigned. + if (!signal->hasDescriptors()) + return; + + auto eventPacket = signal->createDecriptorChangedPacket(valueChanged, domainChanged); + onPacketCallback(signal->getSignalId(), eventPacket); +} + +std::vector StreamingClient::findDataSignalsByTableId(const std::string& tableId) +{ + std::vector result; + for (const auto& [_, inputSignal] : availableSignals) + { + if (inputSignal && !isPlaceHolderSignal(inputSignal) && tableId == inputSignal->getTableId() && !inputSignal->isDomainSignal()) + { + result.push_back(inputSignal); + } + } + for (const auto& [_, inputSignal] : hiddenSignals) + { + if (inputSignal && !isPlaceHolderSignal(inputSignal) && tableId == inputSignal->getTableId() && !inputSignal->isDomainSignal()) + { + result.push_back(inputSignal); + } + } + return result; +} + +InputSignalBasePtr StreamingClient::findTimeSignalByTableId(const std::string& tableId) +{ + std::vector> result; + for (const auto& [_, inputSignal] : availableSignals) + { + if (inputSignal && tableId == inputSignal->getTableId() && inputSignal->isDomainSignal()) + { + return inputSignal; + } + } + for (const auto& [_, inputSignal] : hiddenSignals) + { + if (inputSignal && tableId == inputSignal->getTableId() && inputSignal->isDomainSignal()) + { + return inputSignal; + } + } + return nullptr; +} + +void StreamingClient::onSignal(const daq::streaming_protocol::SubscribedSignal& subscribedSignal, const nlohmann::json& params) +{ + try + { + auto errorGuard = DAQ_ERROR_GUARD(); + { + LOG_I("Signal #{}; signalId {}; tableId {}; name {}; value type {}; Json parameters: \n\n{}\n", + subscribedSignal.signalNumber(), + subscribedSignal.signalId(), + subscribedSignal.tableId(), + subscribedSignal.memberName(), + static_cast(subscribedSignal.dataValueType()), + params.dump()); + } + + if (subscribedSignal.isTimeSignal()) + { + setTimeSignal(subscribedSignal, context); + } + else + { + setDataSignal(subscribedSignal, context); + } + } + catch (const DaqException& e) + { + LOG_W("Failed to interpret received input signal: {}.", e.what()); + } +} + +void StreamingClient::setSignalInitSatisfied(const std::string& signalId) +{ + if (auto iterator = availableSigInitStatus.find(signalId); iterator != availableSigInitStatus.end()) + { + try + { + std::get<0>(iterator->second).set_value(); + } + catch (std::future_error& e) + { + if (e.code() == std::make_error_code(std::future_errc::promise_already_satisfied)) + { + LOG_D("signal {} is already initialized", signalId); + } + else + { + LOG_E("signal {} initialization error {}", signalId, e.what()); + } + } + } +} + +void StreamingClient::checkTmpSubscribedSignalsInit() +{ + if (tmpSubscribedSignalIds.empty()) + return; + + const auto timeout = std::chrono::seconds(1); + auto timeoutExpired = std::chrono::system_clock::now() + timeout; + + for (const auto& [id, params] : availableSigInitStatus) + { + if (tmpSubscribedSignalIds.count(id) != 0) + { + auto status = std::get<1>(params).wait_until(timeoutExpired); + if (status != std::future_status::ready) + { + LOG_W("signal {} has incomplete descriptors", id); + + // publish signal descriptor changes as event packet if signal is subscribed by streaming + if (auto availableSigIt = availableSignals.find(id); availableSigIt != availableSignals.end()) + { + auto inputSignal = availableSigIt->second; + publishSignalChanges(inputSignal, true, true); + } + } + } + } + + // unsubscribe previously subscribed signals + std::vector signalIdsToUnsubscribe; + signalIdsToUnsubscribe.reserve(tmpSubscribedSignalIds.size()); + std::copy_if( + tmpSubscribedSignalIds.begin(), + tmpSubscribedSignalIds.end(), + std::back_inserter(signalIdsToUnsubscribe), + [this](std::string signalId) + { + if (auto availableSigIt = availableSignals.find(signalId); availableSigIt != availableSignals.end()) + { + if (auto inputSignal = availableSigIt->second; inputSignal && inputSignal->getSubscribed()) + return false; + } + return true; + } + ); + protocolHandler->unsubscribe(signalIdsToUnsubscribe); + tmpSubscribedSignalIds.clear(); + + onSignalsInitDone(); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/streaming_server.cpp b/shared/libraries/websocket_streaming/src/streaming_server.cpp new file mode 100644 index 0000000..4aa815c --- /dev/null +++ b/shared/libraries/websocket_streaming/src/streaming_server.cpp @@ -0,0 +1,728 @@ +#include + +#include +#include +#include + +#include +#include "websocket_streaming/streaming_server.h" +#include +#include +#include +#include + +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +using namespace daq::streaming_protocol; +using namespace daq::stream; + +StreamingServer::StreamingServer(const ContextPtr& context) + : work(ioContext.get_executor()) + , logger(context.getLogger()) +{ + if (!this->logger.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException, "Logger must not be null"); + loggerComponent = this->logger.getOrAddComponent("StreamingServer"); + logCallback = [this](spdlog::source_loc location, spdlog::level::level_enum level, const char* msg) { + this->loggerComponent.logMessage(SourceLocation{location.filename, location.line, location.funcname}, msg, static_cast(level)); + }; +} + +StreamingServer::~StreamingServer() +{ + stop(); +} + +void StreamingServer::start(uint16_t port, uint16_t controlPort) +{ + if (serverRunning) + return; + + this->port = port; + + ioContext.restart(); + + auto acceptFunc = [this](StreamPtr stream) { this->onAcceptInternal(stream); }; + + this->server = std::make_unique(ioContext, acceptFunc, port); + this->server->start(); + + auto controlCommandCb = [this](const std::string& streamId, + const std::string& command, + const daq::streaming_protocol::SignalIds& signalIds, + std::string& errorMessage) + { + return onControlCommand(streamId, command, signalIds, errorMessage); + }; + this->controlServer = + std::make_unique(ioContext, + controlPort, + controlCommandCb, + logCallback); + this->controlServer->start(); + + this->serverThread = std::thread([this]() + { + this->ioContext.run(); + LOG_I("Websocket streaming server thread finished"); + }); + + serverRunning = true; +} + +void StreamingServer::stop() +{ + if (!serverRunning) + return; + + ioContext.stop(); + + if (this->server) + this->server->stop(); + if (this->controlServer) + this->controlServer->stop(); + + if (serverThread.get_id() != std::this_thread::get_id()) + { + if (!serverThread.joinable()) + { + LOG_W("Websocket streaming server thread is not joinable"); + } + else + { + serverThread.join(); + LOG_I("Websocket streaming server thread joined"); + } + } + else + { + LOG_C("Websocket streaming server thread cannot join itself"); + } + serverRunning = false; + + this->server.reset(); + this->controlServer.reset(); +} + +void StreamingServer::onAccept(const OnAcceptCallback& callback) +{ + onAcceptCallback = callback; +} + +void StreamingServer::onStartSignalsRead(const OnStartSignalsReadCallback& callback) +{ + onStartSignalsReadCallback = callback; +} + +void StreamingServer::onStopSignalsRead(const OnStopSignalsReadCallback& callback) +{ + onStopSignalsReadCallback = callback; +} + +void StreamingServer::onClientConnected(const OnClientConnectedCallback& callback) +{ + clientConnectedHandler = callback; +} + +void StreamingServer::onClientDisconnected(const OnClientDisconnectedCallback& callback) +{ + clientDisconnectedHandler = callback; +} + +void StreamingServer::broadcastPacket(const std::string& signalId, const PacketPtr& packet) +{ + std::scoped_lock lock(sync); + + for (auto& [_, client] : clients) + { + auto writer = client.first; + auto& outputSignals = client.second; + + if (auto signalIter = outputSignals.find(signalId); signalIter != outputSignals.end()) + { + auto outputSignal = signalIter->second; + + if (auto eventPacket = packet.asPtrOrNull(); eventPacket.assigned()) + { + if (eventPacket.getEventId() == event_packet_id::DATA_DESCRIPTOR_CHANGED) + { + handleDataDescriptorChanges(outputSignal, outputSignals, writer, eventPacket); + } + else + { + STREAMING_PROTOCOL_LOG_W("Event type {} is not supported by streaming.", eventPacket.getEventId()); + } + } + else if (auto dataPacket = packet.asPtrOrNull(); dataPacket.assigned()) + { + outputSignal->writeDaqDataPacket(dataPacket); + } + } + } +} + +DataRuleType StreamingServer::getSignalRuleType(const SignalPtr& signal) +{ + auto descriptor = signal.getDescriptor(); + if (descriptor.assigned() && descriptor.getRule().assigned()) + return descriptor.getRule().getType(); + DAQ_THROW_EXCEPTION(InvalidParameterException, R"(Signal "{}" has incomplete descriptor - unknown signal rule)", signal.getGlobalId()); +} + +OutputDomainSignalBasePtr StreamingServer::addUpdateOrFindDomainSignal(const SignalPtr& domainSignal, + SignalMap& outputSignals, + const StreamWriterPtr& writer) +{ + auto domainSignalId = domainSignal.getGlobalId(); + + OutputDomainSignalBasePtr outputDomainSignal; + if (const auto& outputSignalIt = outputSignals.find(domainSignalId); outputSignalIt != outputSignals.end()) + { + auto outputSignal = outputSignalIt->second; + + if (std::dynamic_pointer_cast(outputSignal)) + { + // replace previously added incomplete placeholder signal + outputDomainSignal = createOutputDomainSignal(domainSignal, domainSignal.getGlobalId(), writer); + outputSignals[domainSignalId] = outputDomainSignal; + } + else + { + // find previously added complete output domain signal + outputDomainSignal = std::dynamic_pointer_cast(outputSignal); + if (!outputDomainSignal) + DAQ_THROW_EXCEPTION(NoInterfaceException, "Previously registered domain output signal {} is not of domain type", domainSignalId); + } + } + else + { + // signal wasn't added before so add it now + outputDomainSignal = createOutputDomainSignal(domainSignal, domainSignal.getGlobalId(), writer); + outputSignals.insert({domainSignalId, outputDomainSignal}); + } + + return outputDomainSignal; +} + +void StreamingServer::addToOutputSignals(const SignalPtr& signal, + SignalMap& outputSignals, + const StreamWriterPtr& writer) +{ + auto domainSignal = signal.getDomainSignal(); + + if (domainSignal.assigned()) + { + // if domain is assigned then signal is considered as value signal + OutputDomainSignalBasePtr outputDomainSignal = addUpdateOrFindDomainSignal(domainSignal, outputSignals, writer); + + auto tableId = domainSignal.getGlobalId().toStdString(); + const auto domainSignalRuleType = getSignalRuleType(domainSignal); + if (domainSignalRuleType == DataRuleType::Linear) + { + auto outputValueSignal = createOutputValueSignal(signal, outputDomainSignal, tableId, writer); + outputSignals.insert_or_assign(signal.getGlobalId(), outputValueSignal); + } + else + { + DAQ_THROW_EXCEPTION(InvalidParameterException, "Unsupported domain signal rule type - only domain signals with linear rule type are supported in LT-streaming"); + } + } + else + { + // if domain is not assigned then signal is considered as domain signal itself + addUpdateOrFindDomainSignal(signal, outputSignals, writer); + } +} + +void StreamingServer::doRead(const std::string& clientId, const daq::stream::StreamPtr& stream) +{ + std::weak_ptr stream_weak = stream; + + // The callback is to be called in the thread that remains active as long as this object exists, + // ensuring that the captured 'this' pointer is always valid. + auto readDoneCallback = [this, stream_weak, clientId](const boost::system::error_code& ec, std::size_t bytesRead) + { + if (auto stream = stream_weak.lock()) + this->onReadDone(clientId, stream, ec, bytesRead); + }; + stream->asyncReadSome(readDoneCallback); +} + +void StreamingServer::onReadDone(const std::string& clientId, + const daq::stream::StreamPtr& stream, + const boost::system::error_code& ec, + std::size_t bytesRead) +{ + if (ec) { + removeClient(clientId); + return; + } + + // any incoming data is ignored + stream->consume(bytesRead); + doRead(clientId, stream); +} + +void StreamingServer::startReadSignals(const ListPtr& signals) +{ + if (onStartSignalsReadCallback && signals.getCount() > 0) + onStartSignalsReadCallback(signals); +} + +void StreamingServer::stopReadSignals(const ListPtr& signals) +{ + if (onStopSignalsReadCallback && signals.getCount() > 0) + onStopSignalsReadCallback(signals); +} + +void StreamingServer::removeClient(const std::string& clientId) +{ + LOG_I("client with id {} disconnected", clientId); + + if (clientDisconnectedHandler) + clientDisconnectedHandler(clientId); + + auto signalsToStopRead = List(); + { + std::scoped_lock lock(sync); + if (auto iter = clients.find(clientId); iter != clients.end()) + { + const auto& outputSignals = iter->second.second; + for (const auto& [signalId, outputSignal] : outputSignals) + { + if (unsubscribeHandler(signalId, outputSignal) && outputSignal->isDataSignal()) + signalsToStopRead.pushBack(outputSignal->getDaqSignal()); + } + clients.erase(iter); + } + } + + stopReadSignals(signalsToStopRead); +} + +void StreamingServer::onAcceptInternal(const daq::stream::StreamPtr& stream) +{ + auto writer = std::make_shared(stream); + writeProtocolInfo(writer); + writeInit(writer); + + auto signals = List(); + + if (onAcceptCallback) + signals = onAcceptCallback(writer); + + auto clientId = stream->endPointUrl(); + LOG_I("New client connected. Stream Id: {}", clientId); + + if (clientConnectedHandler) + clientConnectedHandler(clientId, stream->remoteHost()); + { + std::scoped_lock lock(sync); + clients.insert({clientId, {writer, std::unordered_map()}}); + auto& outputSignals = clients.at(clientId).second; + publishSignalsToClient(writer, signals, outputSignals); + } + + doRead(clientId, stream); +} + +int StreamingServer::onControlCommand(const std::string& streamId, + const std::string& command, + const daq::streaming_protocol::SignalIds& signalIds, + std::string& errorMessage) +{ + if (signalIds.empty()) + { + LOG_W("Signal list is empty, reject command", streamId); + errorMessage = "Signal list is empty"; + return -1; + } + if (command != "subscribe" && command != "unsubscribe") + { + LOG_W("Unknown control command: {}", command); + errorMessage = "Unknown command: " + command; + return -1; + } + + size_t unknownSignalsCount = 0; + std::string message = "Command '" + command + "' failed for unknown signals:\n"; + + auto signalsToStartRead = List(); + auto signalsToStopRead = List(); + + { + std::scoped_lock lock(sync); + + auto clientIter = clients.find(streamId); + if (clientIter == std::end(clients)) + { + LOG_W("Unknown streamId: {}, reject command", streamId); + errorMessage = "Unknown streamId: '" + streamId + "'"; + return -1; + } + + for (const auto& signalId : signalIds) + { + auto& outputSignals = clientIter->second.second; + if (auto signalIter = outputSignals.find(signalId); signalIter != outputSignals.end()) + { + auto outputSignal = signalIter->second; + if (command == "subscribe") + { + if (subscribeHandler(signalId, outputSignal) && outputSignal->isDataSignal()) + signalsToStartRead.pushBack(outputSignal->getDaqSignal()); + } + else if (command == "unsubscribe") + { + if (unsubscribeHandler(signalId, outputSignal) && outputSignal->isDataSignal()) + signalsToStopRead.pushBack(outputSignal->getDaqSignal()); + } + } + else + { + unknownSignalsCount++; + message.append(signalId + "\n"); + } + } + } + + if (command == "subscribe") + startReadSignals(signalsToStartRead); + + if (command == "unsubscribe") + stopReadSignals(signalsToStopRead); + + if (unknownSignalsCount > 0) + { + LOG_W("{}", message); + errorMessage = message; + return -1; + } + else + { + return 0; + } +} + +void StreamingServer::writeProtocolInfo(const daq::streaming_protocol::StreamWriterPtr& writer) +{ + nlohmann::json msg; + msg[METHOD] = META_METHOD_APIVERSION; + msg[PARAMS][VERSION] = OPENDAQ_LT_STREAM_VERSION; + writer->writeMetaInformation(0, msg); +} + +void StreamingServer::writeSignalsAvailable(const daq::streaming_protocol::StreamWriterPtr& writer, + const std::vector& signalIds) +{ + nlohmann::json msg; + msg[METHOD] = META_METHOD_AVAILABLE; + msg[PARAMS][META_SIGNALIDS] = signalIds; + writer->writeMetaInformation(0, msg); +} + +void StreamingServer::writeSignalsUnavailable(const daq::streaming_protocol::StreamWriterPtr& writer, + const std::vector& signalIds) +{ + nlohmann::json msg; + msg[METHOD] = META_METHOD_UNAVAILABLE; + msg[PARAMS][META_SIGNALIDS] = signalIds; + writer->writeMetaInformation(0, msg); +} + +void StreamingServer::writeInit(const streaming_protocol::StreamWriterPtr& writer) +{ + nlohmann::json initMeta; + initMeta[METHOD] = META_METHOD_INIT; + initMeta[PARAMS][META_STREAMID] = writer->id(); + + nlohmann::json jsonRpcHttp; + jsonRpcHttp["httpMethod"] = "POST"; + jsonRpcHttp["httpPath"] = "/"; + jsonRpcHttp["httpVersion"] = "1.1"; + jsonRpcHttp["port"] = std::to_string(controlServer->getPort()); + + nlohmann::json commandInterfaces; + commandInterfaces["jsonrpc-http"] = jsonRpcHttp; + + initMeta[PARAMS][COMMANDINTERFACES] = commandInterfaces; + writer->writeMetaInformation(0, initMeta); +} + +bool StreamingServer::isSignalSubscribed(const std::string& signalId) const +{ + bool result = false; + for (const auto& [_, client] : clients) + { + auto signals = client.second; + if (auto iter = signals.find(signalId); iter != signals.end()) + result = result || iter->second->isSubscribed(); + } + return result; +} + +bool StreamingServer::subscribeHandler(const std::string& signalId, OutputSignalBasePtr signal) +{ + // returns true if signal wasn't subscribed by any client + bool result = !isSignalSubscribed(signalId); + signal->setSubscribed(true); + + return result; +} + +bool StreamingServer::unsubscribeHandler(const std::string& signalId, OutputSignalBasePtr signal) +{ + if (!signal->isSubscribed()) + return false; + signal->setSubscribed(false); + + // returns true if signal became not subscribed by any client + return !isSignalSubscribed(signalId); +} + +void StreamingServer::addSignals(const ListPtr& signals) +{ + std::scoped_lock lock(sync); + for (auto& [_, client] : clients) + { + auto writer = client.first; + auto& outputSignals = client.second; + + publishSignalsToClient(writer, signals, outputSignals); + } +} + +void StreamingServer::removeComponentSignals(const StringPtr& componentId) +{ + auto signalsToStopRead = List(); + { + std::scoped_lock lock(sync); + auto removedComponentId = componentId.toStdString(); + + for (auto& [_, client] : clients) + { + auto writer = client.first; + auto& outputSignals = client.second; + + std::vector signalsToRemove; + + for (const auto& [signalId, outputSignal] : outputSignals) + { + // removed component is a signal, or signal is a descendant of removed component + if (signalId == removedComponentId || IdsParser::isNestedComponentId(removedComponentId, signalId)) + { + signalsToRemove.push_back(signalId); + if (unsubscribeHandler(signalId, outputSignal) && outputSignal->isDataSignal()) + signalsToStopRead.pushBack(outputSignal->getDaqSignal()); + } + } + + if (!signalsToRemove.empty()) + { + writeSignalsUnavailable(writer, signalsToRemove); + for (const auto& signalId : signalsToRemove) + outputSignals.erase(signalId); + } + } + } + + stopReadSignals(signalsToStopRead); +} + +void StreamingServer::updateComponentSignals(const DictPtr& signals, const StringPtr& componentId) +{ + auto signalsToStopRead = List(); + { + std::scoped_lock lock(sync); + auto updatedComponentId = componentId.toStdString(); + + for (auto& [_, client] : clients) + { + auto writer = client.first; + auto& outputSignals = client.second; + + auto signalsToAdd = List(); + for (const auto& [signalId, signal] : signals) + { + if (auto iter = outputSignals.find(signalId.toStdString()); iter == outputSignals.end()) + signalsToAdd.pushBack(signal); + } + if (signalsToAdd.getCount() > 0) + publishSignalsToClient(writer, signalsToAdd, outputSignals); + + std::vector signalsToRemove; + for (const auto& [signalId, outputSignal] : outputSignals) + { + // signal is a descendant of updated component + if (IdsParser::isNestedComponentId(updatedComponentId, signalId) && + (!signals.hasKey(signalId) || !signals.get(signalId).getPublic())) + { + signalsToRemove.push_back(signalId); + if (unsubscribeHandler(signalId, outputSignal) && outputSignal->isDataSignal()) + signalsToStopRead.pushBack(outputSignal->getDaqSignal()); + } + } + if (!signalsToRemove.empty()) + { + writeSignalsUnavailable(writer, signalsToRemove); + for (const auto& signalId : signalsToRemove) + outputSignals.erase(signalId); + } + } + } + + stopReadSignals(signalsToStopRead); +} + +/// Due to the type-specific "hardcoded" implementations of output signals, +/// when a signal descriptor change is incompatible with the existing output signal (e.g. sample type or rule changed), +/// the signal is replaced with a newly created one. To handle this correctly on both the server and client sides, +/// the old signal is marked as unavailable by server, and the new one is made available under the same signal ID. +/// This ensures any cached signal details across server and client implementations are cleared. +/// As a result, the corresponding signal in the LT pseudo device is also removed and the new one appeared. +void StreamingServer::handleDataDescriptorChanges(OutputSignalBasePtr& outputSignal, + SignalMap& outputSignals, + const StreamWriterPtr& writer, + const EventPacketPtr& packet) +{ + const auto [valueDescriptorChanged, domainDescriptorChanged, newValueDescriptor, newDomainDescriptor] = + parseDataDescriptorEventPacket(packet); + bool subscribed = outputSignal->isSubscribed(); + + if (auto placeholderValueSignal = std::dynamic_pointer_cast(outputSignal)) + { + if ((valueDescriptorChanged && newValueDescriptor.assigned()) || + (domainDescriptorChanged && newDomainDescriptor.assigned())) + updateOutputPlaceholderSignal(outputSignal, outputSignals, writer, subscribed); + } + else + { + const auto daqValueSignal = outputSignal->getDaqSignal(); + const auto valueSignalId = daqValueSignal.getGlobalId(); + + if (valueDescriptorChanged) + { + try + { + auto errorGuard = DAQ_ERROR_GUARD(); + outputSignal->writeValueDescriptorChanges(newValueDescriptor); + } + catch (const DaqException& e) + { + writeSignalsUnavailable(writer, {valueSignalId}); + LOG_W("Failed to change value descriptor for signal {}, reason: {}", valueSignalId, e.what()); + outputSignals.insert_or_assign(valueSignalId, std::make_shared(daqValueSignal, logCallback)); + outputSignal = outputSignals.at(valueSignalId); + if (newValueDescriptor.assigned()) + updateOutputPlaceholderSignal(outputSignal, outputSignals, writer, false); + writeSignalsAvailable(writer, {valueSignalId}); + outputSignal->setSubscribed(subscribed); + } + } + + if (domainDescriptorChanged) + { + if (const auto daqDomainSignal = daqValueSignal.getDomainSignal(); daqDomainSignal.assigned()) + { + try + { + auto errorGuard = DAQ_ERROR_GUARD(); + outputSignal->writeDomainDescriptorChanges(newDomainDescriptor); + } + catch (const DaqException& e) + { + LOG_W("Failed to change domain descriptor for signal {}, reason: {}", valueSignalId, e.what()); + const auto domainSignalId = daqDomainSignal.getGlobalId(); + writeSignalsUnavailable(writer, {domainSignalId, valueSignalId}); + outputSignals.insert_or_assign(valueSignalId, std::make_shared(daqValueSignal, logCallback)); + outputSignals.insert_or_assign(domainSignalId, std::make_shared(daqDomainSignal, logCallback)); + outputSignal = outputSignals.at(valueSignalId); + if (newDomainDescriptor.assigned()) + updateOutputPlaceholderSignal(outputSignal, outputSignals, writer, false); + writeSignalsAvailable(writer, {valueSignalId, domainSignalId}); + outputSignal->setSubscribed(subscribed); + } + } + } + } +} + +void StreamingServer::updateOutputPlaceholderSignal(OutputSignalBasePtr& outputSignal, + SignalMap& outputSignals, + const StreamWriterPtr& writer, + bool subscribed) +{ + auto daqSignal = outputSignal->getDaqSignal(); + auto signalId = daqSignal.getGlobalId().toStdString(); + + LOG_I("Parameters of unsupported signal {} has been changed, check if it is supported now ...", daqSignal.getGlobalId()); + try + { + auto errorGuard = DAQ_ERROR_GUARD(); + addToOutputSignals(daqSignal, outputSignals, writer); + outputSignal = outputSignals.at(signalId); + outputSignal->setSubscribed(subscribed); + } + catch (const DaqException& e) + { + LOG_W("Failed to re-create an output LT streaming signal for {}, reason: {}", daqSignal.getGlobalId(), e.what()); + } +} + +void StreamingServer::publishSignalsToClient(const StreamWriterPtr& writer, + const ListPtr& signals, + SignalMap& outputSignals) +{ + std::vector filteredSignalsIds; + for (const auto& daqSignal : signals) + { + if (!daqSignal.getPublic()) + continue; + + auto signalId = daqSignal.getGlobalId().toStdString(); + filteredSignalsIds.push_back(signalId); + + try + { + auto errorGuard = DAQ_ERROR_GUARD(); + addToOutputSignals(daqSignal, outputSignals, writer); + } + catch (const DaqException& e) + { + LOG_W("Failed to create an output LT streaming signal for {}, reason: {}", signalId, e.what()); + auto placeholderSignal = std::make_shared(daqSignal, logCallback); + outputSignals.insert({signalId, placeholderSignal}); + } + } + + writeSignalsAvailable(writer, filteredSignalsIds); +} + +OutputDomainSignalBasePtr StreamingServer::createOutputDomainSignal(const SignalPtr& daqDomainSignal, + const std::string& tableId, + const StreamWriterPtr& writer) +{ + const auto domainSignalRuleType = getSignalRuleType(daqDomainSignal); + + if (domainSignalRuleType == DataRuleType::Linear) + return std::make_shared(writer, daqDomainSignal, tableId, logCallback); + DAQ_THROW_EXCEPTION(InvalidParameterException, "Unsupported domain signal rule type"); +} + +OutputSignalBasePtr StreamingServer::createOutputValueSignal(const SignalPtr& daqSignal, + const OutputDomainSignalBasePtr& outputDomainSignal, + const std::string& tableId, + const StreamWriterPtr& writer) +{ + const auto valueSignalRuleType = getSignalRuleType(daqSignal); + + if (valueSignalRuleType == DataRuleType::Explicit) + return std::make_shared(writer, daqSignal, outputDomainSignal, tableId, logCallback); + else if (valueSignalRuleType == DataRuleType::Constant) + return std::make_shared(writer, daqSignal, outputDomainSignal, tableId, logCallback); + DAQ_THROW_EXCEPTION(InvalidParameterException, "Unsupported value signal rule type"); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/websocket_client_device_impl.cpp b/shared/libraries/websocket_streaming/src/websocket_client_device_impl.cpp new file mode 100644 index 0000000..fb09862 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/websocket_client_device_impl.cpp @@ -0,0 +1,227 @@ +#include "websocket_streaming/websocket_client_device_impl.h" +#include +#include +#include +#include "websocket_streaming/websocket_client_signal_factory.h" +#include "websocket_streaming/websocket_streaming_factory.h" +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +WebsocketClientDeviceImpl::WebsocketClientDeviceImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& localId, + const StringPtr& connectionString) + : Device(ctx, parent, localId) + , connectionString(connectionString) +{ + if (!this->connectionString.assigned()) + DAQ_THROW_EXCEPTION(ArgumentNullException, "connectionString cannot be null"); + this->name = "WebsocketClientPseudoDevice"; + createWebsocketStreaming(); + activateStreaming(); +} + +void WebsocketClientDeviceImpl::removed() +{ + websocketStreaming.release(); + Device::removed(); +} + +DeviceInfoPtr WebsocketClientDeviceImpl::onGetInfo() +{ + return DeviceInfo(connectionString, "WebsocketClientPseudoDevice"); +} + +void WebsocketClientDeviceImpl::activateStreaming() +{ + websocketStreaming.setActive(true); + for (const auto& [_, signal] : deviceSignals) + { + websocketStreaming.addSignals({signal}); + auto mirroredSignalConfigPtr = signal.template asPtr(); + mirroredSignalConfigPtr.setActiveStreamingSource(websocketStreaming.getConnectionString()); + } +} + +/// connects to streaming server and waits till the list of available signals received +void WebsocketClientDeviceImpl::createWebsocketStreaming() +{ + bool useRawTcpConnection = connectionString.toStdString().find("daq.tcp://") == 0; + auto streamingClient = std::make_shared(context, connectionString, useRawTcpConnection); + + auto signalInitCallback = [this](const StringPtr& signalId, const SubscribedSignalInfo& sInfo) + { + this->onSignalInit(signalId, sInfo); + }; + streamingClient->onDeviceAvailableSignalInit(signalInitCallback); + + auto signalUpdatedCallback = [this](const StringPtr& signalId, const SubscribedSignalInfo& sInfo) + { + this->onSignalUpdated(signalId, sInfo); + }; + streamingClient->onDeviceSignalUpdated(signalUpdatedCallback); + + auto domainSignalInitCallback = [this](const StringPtr& dataSignalId,const StringPtr& domainSignalId) + { + this->onDomainSignalInit(dataSignalId, domainSignalId); + }; + streamingClient->onDeviceDomainSingalInit(domainSignalInitCallback); + + auto availableSignalsCallback = [this](const std::vector& signalIds) + { + this->registerAvailableSignals(signalIds); + }; + streamingClient->onDeviceAvailableSignals(availableSignalsCallback); + + auto unavailableSignalsCallback = [this](const std::vector& signalIds) + { + this->removeSignals(signalIds); + }; + streamingClient->onDeviceUnavailableSignals(unavailableSignalsCallback); + + auto hiddenSignalCallback = [this](const StringPtr& signalId, const SubscribedSignalInfo& sInfo) + { + this->registerHiddenSignal(signalId, sInfo); + }; + streamingClient->onDeviceHiddenSignal(hiddenSignalCallback); + + auto signalsInitDoneCallback = [this]() + { + this->addInitializedSignals(); + }; + streamingClient->onDeviceSignalsInitDone(signalsInitDoneCallback); + + websocketStreaming = WebsocketStreaming(streamingClient, connectionString, context); +} + +void WebsocketClientDeviceImpl::onSignalInit(const StringPtr& signalId, const SubscribedSignalInfo& sInfo) +{ + if (!sInfo.dataDescriptor.assigned()) + return; + + if (auto signalIt = deviceSignals.find(signalId); signalIt != deviceSignals.end()) + { + // sets signal name as it appeared in metadata "Name" + signalIt->second.asPtr().unlockAllAttributes(); + signalIt->second.setName(sInfo.signalName); + signalIt->second.asPtr().lockAllAttributes(); + + signalIt->second.asPtr().setMirroredDataDescriptor(sInfo.dataDescriptor); + updateSignalProperties(signalIt->second, sInfo); + } +} + +void WebsocketClientDeviceImpl::onSignalUpdated(const StringPtr& signalId, const SubscribedSignalInfo& sInfo) +{ + if (!sInfo.dataDescriptor.assigned()) + return; + + if (auto signalIt = deviceSignals.find(signalId); signalIt != deviceSignals.end()) + updateSignalProperties(signalIt->second, sInfo); +} + +void WebsocketClientDeviceImpl::onDomainSignalInit(const StringPtr& signalId, const StringPtr& domainSignalId) +{ + // Sets domain signal for data signal + if (auto dataSignalIt = deviceSignals.find(signalId); dataSignalIt != deviceSignals.end()) + { + auto domainSignalIt = deviceSignals.find(domainSignalId); + if (domainSignalIt != deviceSignals.end()) + { + // domain signal is found in device + auto domainSignal = domainSignalIt->second; + auto dataSignal = dataSignalIt->second; + dataSignal.asPtr().setMirroredDomainSignal(domainSignal); + } + } +} + +void WebsocketClientDeviceImpl::registerAvailableSignals(const std::vector& signalIds) +{ + // Adds to device only signals published by server explicitly and in the same order these were published + for (const auto& signalId : signalIds) + { + // TODO Streaming do not support some types of signals and do not provide META info - + // - for those the mirrored signals will stay incomplete (without descriptors set) + auto signal = WebsocketClientSignal(this->context, this->signals, signalId); + deviceSignals.insert({signalId, signal}); + orderedSignalIds.push_back(signalId); + } +} + +void WebsocketClientDeviceImpl::removeSignals(const std::vector& signalIds) +{ + for (const auto& removedSignalId : signalIds) + { + auto signalIter = deviceSignals.find(removedSignalId); + if (signalIter == deviceSignals.end()) + { + LOG_E("Signal with id {} is not found in LT streaming device", removedSignalId); + continue; + } + + auto signalToRemove = signalIter->second; + + // recreate signal -> domainSignal relations in the same way as on server + for (const auto& [_, dataSignal] : deviceSignals) + { + auto domainSignal = dataSignal.getDomainSignal(); + if (domainSignal.assigned() && domainSignal.asPtr().getRemoteId() == removedSignalId) + { + dataSignal.asPtr().setMirroredDomainSignal(nullptr); + } + } + + this->removeSignal(signalToRemove); + deviceSignals.erase(signalIter); + orderedSignalIds.erase(std::remove(orderedSignalIds.begin(), orderedSignalIds.end(), removedSignalId), orderedSignalIds.end()); + } +} + +void WebsocketClientDeviceImpl::registerHiddenSignal(const StringPtr& signalId, const SubscribedSignalInfo& sInfo) +{ + // Adds 'hidden' signal which were not published by server explicitly as available + auto signal = WebsocketClientSignal(this->context, this->signals, signalId); + deviceSignals.insert({signalId, signal}); + onSignalInit(signalId, sInfo); + orderedSignalIds.push_back(signalId.toStdString()); +} + +void WebsocketClientDeviceImpl::addInitializedSignals() +{ + for (const auto& signalId : orderedSignalIds) + { + if (auto signalIt = deviceSignals.find(String(signalId)); signalIt != deviceSignals.end()) + { + auto signal = signalIt->second; + + if (!this->signals.findComponent(signal.getLocalId()).assigned()) + { + if (websocketStreaming.assigned()) + { + websocketStreaming.addSignals({signal}); + auto mirroredSignalConfigPtr = signal.template asPtr(); + mirroredSignalConfigPtr.setActiveStreamingSource(websocketStreaming.getConnectionString()); + } + + this->addSignal(signal); + } + } + } +} + +void WebsocketClientDeviceImpl::updateSignalProperties(const SignalPtr& signal, const SubscribedSignalInfo& sInfo) +{ + signal.asPtr().unlockAllAttributes(); + + if (sInfo.signalProps.name.has_value()) + signal.setName(sInfo.signalProps.name.value()); + if (sInfo.signalProps.description.has_value()) + signal.setDescription(sInfo.signalProps.description.value()); + + signal.asPtr().lockAllAttributes(); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/websocket_client_signal_impl.cpp b/shared/libraries/websocket_streaming/src/websocket_client_signal_impl.cpp new file mode 100644 index 0000000..9fee184 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/websocket_client_signal_impl.cpp @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include "websocket_streaming/websocket_client_signal_impl.h" + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +static constexpr char delimeter = '#'; + +WebsocketClientSignalImpl::WebsocketClientSignalImpl(const ContextPtr& ctx, + const ComponentPtr& parent, + const StringPtr& streamingId) + : MirroredSignal(ctx, parent, CreateLocalId(streamingId), nullptr) + , streamingId(streamingId) +{ +} + +StringPtr WebsocketClientSignalImpl::CreateLocalId(const StringPtr& streamingId) +{ + std::string localId = streamingId; + + const char slash = '/'; + std::replace(localId.begin(), localId.end(), slash, delimeter); + + return String(localId); +} + +StringPtr WebsocketClientSignalImpl::onGetRemoteId() const +{ + return streamingId; +} + +Bool WebsocketClientSignalImpl::onTriggerEvent(const EventPacketPtr& eventPacket) +{ + return Self::onTriggerEvent(eventPacket); +} + +SignalPtr WebsocketClientSignalImpl::onGetDomainSignal() +{ + return mirroredDomainSignal.addRefAndReturn(); +} + +DataDescriptorPtr WebsocketClientSignalImpl::onGetDescriptor() +{ + return mirroredDataDescriptor.addRefAndReturn(); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/websocket_streaming_impl.cpp b/shared/libraries/websocket_streaming/src/websocket_streaming_impl.cpp new file mode 100644 index 0000000..b30fff5 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/websocket_streaming_impl.cpp @@ -0,0 +1,113 @@ +#include "websocket_streaming/websocket_streaming_impl.h" +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +WebsocketStreamingImpl::WebsocketStreamingImpl(const StringPtr& connectionString, + const ContextPtr& context) + : WebsocketStreamingImpl(std::make_shared(context, connectionString), + connectionString, + context) +{ +} + +WebsocketStreamingImpl::WebsocketStreamingImpl(StreamingClientPtr streamingClient, + const StringPtr& connectionString, + const ContextPtr& context) + : Streaming(connectionString, context, true) + , streamingClient(streamingClient) +{ + prepareStreamingClient(); + if (!this->streamingClient->connect()) + DAQ_THROW_EXCEPTION(NotFoundException, "Failed to connect to streaming server url: {}", connectionString); +} + +void WebsocketStreamingImpl::onSetActive(bool active) +{ +} + +void WebsocketStreamingImpl::onAddSignal(const MirroredSignalConfigPtr& signal) +{ +} + +void WebsocketStreamingImpl::onRemoveSignal(const MirroredSignalConfigPtr& /*signal*/) +{ +} + +void WebsocketStreamingImpl::onSubscribeSignal(const StringPtr& signalStreamingId) +{ + streamingClient->subscribeSignal(signalStreamingId.toStdString()); +} + +void WebsocketStreamingImpl::onUnsubscribeSignal(const StringPtr& signalStreamingId) +{ + streamingClient->unsubscribeSignal(signalStreamingId.toStdString()); +} + +void WebsocketStreamingImpl::prepareStreamingClient() +{ + auto packetCallback = [this](const StringPtr& signalId, const PacketPtr& packet) + { + this->onPacket(signalId, packet); + }; + streamingClient->onPacket(packetCallback); + + auto availableSignalsCallback = [this](const std::vector& signalIds) + { + this->onAvailableSignals(signalIds); + }; + streamingClient->onStreamingAvailableSignals(availableSignalsCallback); + + auto unavailableSignalsCallback = [this](const std::vector& signalIds) + { + this->onUnavailableSignals(signalIds); + }; + streamingClient->onStreamingUnavailableSignals(unavailableSignalsCallback); + + auto hiddenSignalCallback = [this](const StringPtr& signalId, const SubscribedSignalInfo& /*sInfo*/) + { + this->onHiddenSignal(signalId); + }; + streamingClient->onStreamingHiddenSignal(hiddenSignalCallback); + + auto signalSubscriptionAckCallback = [this](const std::string& signalStringId, bool subscribed) + { + this->triggerSubscribeAck(signalStringId, subscribed); + }; + streamingClient->onSubscriptionAck(signalSubscriptionAckCallback); +} + +void WebsocketStreamingImpl::onAvailableSignals(const std::vector& signalIds) +{ + for (const auto& signalId : signalIds) + { + auto signalStringId = String(signalId); + addToAvailableSignals(signalStringId); + } +} + +void WebsocketStreamingImpl::onUnavailableSignals(const std::vector& signalIds) +{ + for (const auto& signalId : signalIds) + { + auto signalStringId = String(signalId); + removeFromAvailableSignals(signalStringId); + } +} + +void WebsocketStreamingImpl::onHiddenSignal(const std::string& signalId) +{ + if (const auto it = hiddenSignals.find(signalId); it == hiddenSignals.end()) + { + hiddenSignals.insert(signalId); + + auto signalStringId = String(signalId); + addToAvailableSignals(signalStringId); + } +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/src/websocket_streaming_server.cpp b/shared/libraries/websocket_streaming/src/websocket_streaming_server.cpp new file mode 100644 index 0000000..91f7fa5 --- /dev/null +++ b/shared/libraries/websocket_streaming/src/websocket_streaming_server.cpp @@ -0,0 +1,208 @@ +#include +#include +#include "websocket_streaming/websocket_streaming_server.h" +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +WebsocketStreamingServer::~WebsocketStreamingServer() +{ + this->context.getOnCoreEvent() -= event(this, &WebsocketStreamingServer::coreEventCallback); + stopInternal(); +} + +WebsocketStreamingServer::WebsocketStreamingServer(const InstancePtr& instance) + : WebsocketStreamingServer(instance.getRootDevice(), instance.getContext()) +{ +} + +WebsocketStreamingServer::WebsocketStreamingServer(const DevicePtr& device, const ContextPtr& context) + : device(device) + , context(context) + , streamingServer(context) + , packetReader(device, context) + , loggerComponent(context.getLogger().getOrAddComponent("WebsocketStreamingServer")) +{ +} + +void WebsocketStreamingServer::setStreamingPort(uint16_t port) +{ + this->streamingPort = port; +} + +void WebsocketStreamingServer::setControlPort(uint16_t port) +{ + this->controlPort = port; +} + +void WebsocketStreamingServer::start() +{ + if (!device.assigned()) + DAQ_THROW_EXCEPTION(InvalidStateException, "Device is not set."); + if (!context.assigned()) + DAQ_THROW_EXCEPTION(InvalidStateException, "Context is not set."); + if (streamingPort == 0 || controlPort == 0) + return; + + auto info = this->device.getInfo(); + if (info.hasServerCapability("OpenDAQLTStreaming")) + DAQ_THROW_EXCEPTION(InvalidStateException, fmt::format("Device \"{}\" already has an OpenDAQLTStreaming server capability.", info.getName())); + + streamingServer.onAccept([this](const daq::streaming_protocol::StreamWriterPtr& writer) { return device.getSignals(search::Recursive(search::Any())); }); + streamingServer.onStartSignalsRead([this](const ListPtr& signals) { packetReader.startReadSignals(signals); } ); + streamingServer.onStopSignalsRead([this](const ListPtr& signals) { packetReader.stopReadSignals(signals); } ); + streamingServer.onClientConnected( + [this](const std::string& clientId, const std::string& address) + { + SizeT clientNumber = 0; + if (device.assigned() && !device.isRemoved()) + { + device.getInfo().asPtr(true).addConnectedClient( + &clientNumber, + ConnectedClientInfo(address, ProtocolType::Streaming, "OpenDAQLTStreaming", "", "")); + } + registeredClientIds.insert({clientId, clientNumber}); + } + ); + streamingServer.onClientDisconnected( + [this](const std::string& clientId) + { + if (auto it = registeredClientIds.find(clientId); it != registeredClientIds.end()) + { + if (device.assigned() && !device.isRemoved() && it->second != 0) + { + device.getInfo().asPtr(true).removeConnectedClient(it->second); + } + registeredClientIds.erase(it); + } + } + ); + streamingServer.start(streamingPort, controlPort); + + packetReader.setLoopFrequency(50); + packetReader.onPacket([this](const SignalPtr& signal, const ListPtr& packets) + { + const auto signalId = signal.getGlobalId(); + for (const auto& packet : packets) + streamingServer.broadcastPacket(signalId, packet); + }); + packetReader.start(); + + this->context.getOnCoreEvent() += event(this, &WebsocketStreamingServer::coreEventCallback); + + const ServerCapabilityConfigPtr serverCapability = ServerCapability("OpenDAQLTStreaming", "OpenDAQLTStreaming", ProtocolType::Streaming); + serverCapability.setPrefix("daq.lt"); + serverCapability.setPort(streamingPort); + serverCapability.setConnectionType("TCP/IP"); + info.asPtr(true).addServerCapability(serverCapability); +} + +void WebsocketStreamingServer::stop() +{ + if (device.assigned() && !device.isRemoved()) + { + const auto info = this->device.getInfo(); + const auto infoInternal = info.asPtr(); + if (info.hasServerCapability("OpenDAQLTStreaming")) + infoInternal.removeServerCapability("OpenDAQLTStreaming"); + for (const auto& [_, clientNumber] : registeredClientIds) + { + if (clientNumber != 0) + infoInternal.removeConnectedClient(clientNumber); + } + } + registeredClientIds.clear(); + + stopInternal(); +} + +void WebsocketStreamingServer::stopInternal() +{ + packetReader.stop(); + streamingServer.stop(); +} + +void WebsocketStreamingServer::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; + } +} + +DictPtr WebsocketStreamingServer::getSignalsOfComponent(ComponentPtr& component) +{ + auto signals = Dict(); + if (component.supportsInterface()) + { + signals.set(component.getGlobalId(), component.asPtr()); + } + else if (component.supportsInterface()) + { + auto nestedComponents = component.asPtr().getItems(search::Recursive(search::Any())); + for (const auto& nestedComponent : nestedComponents) + { + if (nestedComponent.supportsInterface()) + signals.set(nestedComponent.getGlobalId(), nestedComponent.asPtr()); + } + } + return signals; +} + +void WebsocketStreamingServer::componentAdded(ComponentPtr& /*sender*/, CoreEventArgsPtr& eventArgs) +{ + ComponentPtr addedComponent = eventArgs.getParameters().get("Component"); + + auto deviceGlobalId = device.getGlobalId().toStdString(); + auto addedComponentGlobalId = addedComponent.getGlobalId().toStdString(); + if (addedComponentGlobalId.find(deviceGlobalId) != 0) + return; + + LOG_I("Added Component: {};", addedComponentGlobalId); + streamingServer.addSignals(getSignalsOfComponent(addedComponent).getValueList()); +} + +void WebsocketStreamingServer::componentRemoved(ComponentPtr& sender, CoreEventArgsPtr& eventArgs) +{ + StringPtr removedComponentLocalId = eventArgs.getParameters().get("Id"); + + auto deviceGlobalId = device.getGlobalId().toStdString(); + auto removedComponentGlobalId = + sender.getGlobalId().toStdString() + "/" + removedComponentLocalId.toStdString(); + if (removedComponentGlobalId.find(deviceGlobalId) != 0) + return; + + LOG_I("Component: {}; is removed", removedComponentGlobalId); + streamingServer.removeComponentSignals(removedComponentGlobalId); +} + +void WebsocketStreamingServer::componentUpdated(ComponentPtr& updatedComponent) +{ + auto deviceGlobalId = device.getGlobalId().toStdString(); + auto updatedComponentGlobalId = updatedComponent.getGlobalId().toStdString(); + if (updatedComponentGlobalId.find(deviceGlobalId) != 0) + return; + + LOG_I("Component: {}; is updated", updatedComponentGlobalId); + + // update list of known signals to include added and exclude removed signals + streamingServer.updateComponentSignals(getSignalsOfComponent(updatedComponent), updatedComponentGlobalId); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/tests/CMakeLists.txt b/shared/libraries/websocket_streaming/tests/CMakeLists.txt new file mode 100644 index 0000000..3f63dc5 --- /dev/null +++ b/shared/libraries/websocket_streaming/tests/CMakeLists.txt @@ -0,0 +1,35 @@ +set(MODULE_NAME websocket_streaming) +set(TEST_APP test_${MODULE_NAME}) + +add_executable(${TEST_APP} + streaming_test_helpers.h + test_signal_descriptor_converter.cpp + test_streaming.cpp + test_websocket_client_device.cpp + test_signal_generator.cpp + test_app.cpp +) + +if (MSVC) + target_compile_options(${TEST_APP} PRIVATE /bigobj) +endif() + +target_link_libraries(${TEST_APP} PRIVATE + ${SDK_TARGET_NAMESPACE}::${MODULE_NAME} + daq::opendaq + daq::opendaq_mocks + daq::streaming_protocol + ${SDK_TARGET_NAMESPACE}::test_utils +) + +set_target_properties(${TEST_APP} PROPERTIES DEBUG_POSTFIX _debug) +target_include_directories(${TEST_APP} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") + +add_test(NAME ${TEST_APP} + COMMAND $ + WORKING_DIRECTORY $ +) + +if(OPENDAQ_ENABLE_COVERAGE) + setup_target_for_coverage(${MODULE_NAME}coverage ${TEST_APP} ${MODULE_NAME}coverage) +endif() diff --git a/shared/libraries/websocket_streaming/tests/streaming_test_helpers.h b/shared/libraries/websocket_streaming/tests/streaming_test_helpers.h new file mode 100644 index 0000000..484e3a4 --- /dev/null +++ b/shared/libraries/websocket_streaming/tests/streaming_test_helpers.h @@ -0,0 +1,134 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include "streaming_protocol/Unit.hpp" + +// MAC CI issue +#if !defined(SKIP_TEST_MAC_CI) +# if defined(__clang__) +# define SKIP_TEST_MAC_CI GTEST_SKIP() << "Skipping test on MacOs" +# else +# define SKIP_TEST_MAC_CI +# endif +#endif + +namespace streaming_test_helpers +{ + inline daq::InstancePtr createServerInstance() + { + const auto moduleManager = daq::ModuleManager("[[none]]"); + const auto authenticationProvider = daq::AuthenticationProvider(); + auto context = Context(nullptr, daq::Logger(), daq::TypeManager(), moduleManager, authenticationProvider); + const daq::ModulePtr deviceModule(MockDeviceModule_Create(context)); + moduleManager.addModule(deviceModule); + + const daq::ModulePtr fbModule(MockFunctionBlockModule_Create(context)); + moduleManager.addModule(fbModule); + + auto instance = InstanceCustom(context, "localInstance"); + instance.addDevice("daqmock://client_device"); + instance.addDevice("daqmock://phys_device"); + instance.addFunctionBlock("mock_fb_uid"); + + return instance; + } + + inline daq::SignalPtr createLinearTimeSignal(const daq::ContextPtr& ctx) + { + const size_t nanosecondsInSecond = 1000000000; + auto delta = nanosecondsInSecond / 1000; + + auto descriptor = daq::DataDescriptorBuilder() + .setSampleType(daq::SampleType::UInt64) + .setRule(daq::LinearDataRule(delta, 100)) + .setTickResolution(daq::Ratio(1, nanosecondsInSecond)) + .setOrigin(daq::streaming_protocol::UNIX_EPOCH_DATE_UTC_TIME) + .setUnit(daq::Unit("s", + daq::streaming_protocol::Unit::UNIT_ID_SECONDS, + "seconds", + "time")) + .setName("Time") + .build(); + + auto signal = SignalWithDescriptor(ctx, descriptor, nullptr, descriptor.getName()); + + signal.asPtr().enableCoreEventTrigger(); + return signal; + } + + inline daq::SignalPtr createExplicitValueSignal(const daq::ContextPtr& ctx, + const daq::StringPtr& name, + const daq::SignalPtr& domainSignal) + { + auto meta = daq::Dict(); + meta["color"] = "green"; + meta["used"] = "0"; + + auto descriptor = daq::DataDescriptorBuilder() + .setSampleType(daq::SampleType::Float64) + .setUnit(daq::Unit("V", 1, "voltage", "Quantity")) + .setValueRange(daq::Range(0, 10)) + .setRule(daq::ExplicitDataRule()) + .setPostScaling(daq::LinearScaling(1.0, 0.0, daq::SampleType::Int16, daq::ScaledSampleType::Float64)) + .setName(name) + .setMetadata(meta) + .build(); + + auto signal = SignalWithDescriptor(ctx, descriptor, nullptr, descriptor.getName()); + signal.setDomainSignal(domainSignal); + signal.setName(name); + signal.setDescription("TestDescription"); + + signal.asPtr().enableCoreEventTrigger(); + return signal; + } + + inline daq::SignalPtr createConstantValueSignal(const daq::ContextPtr& ctx, + const daq::StringPtr& name, + const daq::SignalPtr& domainSignal) + { + auto descriptor = daq::DataDescriptorBuilder() + .setSampleType(daq::SampleType::UInt64) + .setValueRange(daq::Range(0, 10)) + .setRule(daq::ConstantDataRule()) + .setName(name) + .build(); + + auto timeSignal = createLinearTimeSignal(ctx); + auto signal = SignalWithDescriptor(ctx, descriptor, nullptr, descriptor.getName()); + signal.setDomainSignal(timeSignal); + signal.setName(name); + signal.setDescription("TestDescription"); + + signal.asPtr().enableCoreEventTrigger(); + return signal; + } +} diff --git a/shared/libraries/websocket_streaming/tests/test_app.cpp b/shared/libraries/websocket_streaming/tests/test_app.cpp new file mode 100644 index 0000000..608cd21 --- /dev/null +++ b/shared/libraries/websocket_streaming/tests/test_app.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int argc, char** args) +{ + testing::InitGoogleTest(&argc, args); + + testing::TestEventListeners& listeners = testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new BaseTestListener()); + + return RUN_ALL_TESTS(); +} diff --git a/shared/libraries/websocket_streaming/tests/test_signal_descriptor_converter.cpp b/shared/libraries/websocket_streaming/tests/test_signal_descriptor_converter.cpp new file mode 100644 index 0000000..cd303ce --- /dev/null +++ b/shared/libraries/websocket_streaming/tests/test_signal_descriptor_converter.cpp @@ -0,0 +1,489 @@ +#include + +#include "gtest/gtest.h" + +#include "nlohmann/json.hpp" + +#include "streaming_protocol/Defines.h" +#include "streaming_protocol/SubscribedSignal.hpp" +#include "streaming_protocol/SynchronousSignal.hpp" +#include "streaming_protocol/LinearTimeSignal.hpp" +#include "streaming_protocol/iWriter.hpp" +#include +#include +#include +#include +#include +#include +#include +#include "websocket_streaming/signal_descriptor_converter.h" +#include +#include +#include +#include +#include +#include +#include +#include "streaming_protocol/Logging.hpp" +#include + +namespace bsp = daq::streaming_protocol; + +BEGIN_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING + +static uint64_t TimeTicksPerSecond = 1000000000; + +class DummayWriter : public bsp::iWriter +{ +public: + virtual int writeMetaInformation(unsigned int signalNumber, const nlohmann::json& data) override + { + metaInformations.emplace_back(signalNumber, data); + return 0; + } + virtual int writeSignalData(unsigned int, const void*, size_t) override + { + return 0; + } + + virtual std::string id() const override + { + static unsigned int id = 0; + return std::to_string(id); + ++id; + } + + std::vector> metaInformations; +}; + +SignalPtr createTimeSignal(const ContextPtr& ctx, uint64_t timeTicksPerSecond = TimeTicksPerSecond) +{ + uint64_t timeTickStart = 10000000; + uint64_t outputRateInTicks = bsp::BaseDomainSignal::timeTicksFromNanoseconds(std::chrono::milliseconds(1), timeTicksPerSecond); + + auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleType::UInt64) + .setRule(LinearDataRule(outputRateInTicks, timeTickStart)) + .setTickResolution(Ratio(1, timeTicksPerSecond)) + .setName("Time") + .build(); + + return SignalWithDescriptor(ctx, descriptor, nullptr, descriptor.getName()); +} + +SignalPtr createSineSignal(const ContextPtr& ctx) +{ + std::string signalId = "Sine"; + std::string memberName = "member name"; + int32_t unitId = 42; + std::string unitDisplayName = "some unit"; + + UnitPtr unit = Unit("V", unitId, "voltage"); + auto descriptor = + DataDescriptorBuilder().setSampleType(SampleType::Float64).setName(memberName).setName(memberName).setUnit(unit).build(); + + auto timeSignal = createTimeSignal(ctx); + auto signal = SignalWithDescriptor(ctx, descriptor, nullptr, signalId); + signal.setDomainSignal(timeSignal); + return signal; +} + +SignalPtr createSineSignalWithPostScaling(const ContextPtr& ctx) +{ + std::string signalId = "Sine"; + std::string memberName = "member name"; + int32_t unitId = 42; + std::string unitDisplayName = "some unit"; + + double scale = 1; + double offset = 0; + UnitPtr unit = Unit("V", unitId, "voltage"); + + auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleType::Float64) + .setName(memberName) + .setPostScaling(LinearScaling(scale, offset, SampleType::Int16, ScaledSampleType::Float64)) + .setUnit(unit) + .build(); + + auto timeSignal = createTimeSignal(ctx); + auto signal = SignalWithDescriptor(ctx, descriptor, nullptr, signalId); + signal.setDomainSignal(timeSignal); + return signal; +} + +TEST(SignalConverter, synchronousSignal) +{ + auto signal = createSineSignal(NullContext()); + auto dataDescriptor = signal.getDescriptor(); + auto unit = dataDescriptor.getUnit(); + + auto domainSignal = signal.getDomainSignal(); + auto domainDescriptor = domainSignal.getDescriptor(); + + DummayWriter dummyWriter; + // 1kHz + uint64_t outputRateInTicks = bsp::BaseDomainSignal::timeTicksFromNanoseconds(std::chrono::milliseconds(1), TimeTicksPerSecond); + auto outputRateInNs = std::chrono::milliseconds(1); + auto syncSignal = std::make_shared>(signal.getGlobalId(), "table1", dummyWriter, bsp::Logging::logCallback()); + auto timeSignal = std::make_shared(domainSignal.getGlobalId(), "table1", TimeTicksPerSecond, outputRateInNs, dummyWriter, bsp::Logging::logCallback()); + + ASSERT_NO_THROW(SignalDescriptorConverter::ToStreamedValueSignal(signal, syncSignal, SignalProps{})); + ASSERT_EQ(syncSignal->getUnitId(), unit.getId()); + ASSERT_EQ(syncSignal->getUnitDisplayName(), unit.getSymbol()); + ASSERT_EQ(syncSignal->getMemberName(), signal.getName()); + ASSERT_EQ(syncSignal->getTableId(), "table1"); + + ASSERT_NO_THROW(SignalDescriptorConverter::ToStreamedLinearSignal(domainSignal, timeSignal, SignalProps{})); + ASSERT_EQ(timeSignal->getTimeDelta(), outputRateInTicks); + ASSERT_EQ(timeSignal->getTableId(), "table1"); + + auto start = domainDescriptor.getRule().getParameters().get("start"); + timeSignal->setTimeStart(start); + ASSERT_EQ(timeSignal->getTimeStart(), start); +} + +TEST(SignalConverter, TickResolution) +{ + auto domainSignal = createTimeSignal(NullContext(), TimeTicksPerSecond); + auto domainDescriptor = domainSignal.getDescriptor(); + + DummayWriter dummyWriter; + // 1kHz + auto outputRateInNs = std::chrono::milliseconds(1); + auto timeSignal = std::make_shared(domainSignal.getGlobalId(), "table1", TimeTicksPerSecond, outputRateInNs, dummyWriter, bsp::Logging::logCallback()); + + ASSERT_NO_THROW(SignalDescriptorConverter::ToStreamedLinearSignal(domainSignal, timeSignal, SignalProps{})); + timeSignal->writeSignalMetaInformation(); + + auto getTickResolution = [](const nlohmann::json& data) -> RatioPtr { + using namespace daq::streaming_protocol; + auto timeResolution = data[PARAMS][META_DEFINITION][META_RESOLUTION]; + return Ratio(timeResolution[META_NUMERATOR], timeResolution[META_DENOMINATOR]); + }; + + auto lastMetaInformation = *(--dummyWriter.metaInformations.end()); + ASSERT_EQ(getTickResolution(lastMetaInformation.second), Ratio(1, TimeTicksPerSecond)); + + auto newDomainSignal = createTimeSignal(NullContext(), 1000); + + ASSERT_NO_THROW(SignalDescriptorConverter::ToStreamedLinearSignal(newDomainSignal, timeSignal, SignalProps{})); + timeSignal->writeSignalMetaInformation(); + + lastMetaInformation = *(--dummyWriter.metaInformations.end()); + ASSERT_EQ(getTickResolution(lastMetaInformation.second), Ratio(1, 1000)); +} + +TEST(SignalConverter, synchronousSignalWithPostScaling) +{ + auto signal = createSineSignalWithPostScaling(NullContext()); + auto valueDescriptor = signal.getDescriptor(); + auto unit = valueDescriptor.getUnit(); + + DummayWriter dummyWriter; + // 1kHz + auto syncSignal = std::make_shared>(signal.getGlobalId(), "table1", dummyWriter, bsp::Logging::logCallback()); + + ASSERT_NO_THROW(SignalDescriptorConverter::ToStreamedValueSignal(signal, syncSignal, SignalProps{})); + ASSERT_EQ(syncSignal->getUnitId(), unit.getId()); + ASSERT_EQ(syncSignal->getUnitDisplayName(), unit.getSymbol()); + ASSERT_EQ(syncSignal->getMemberName(), signal.getName()); +} + +TEST(SignalConverter, subscribedDataSignal) +{ + std::string method; + int result; + unsigned int signalNumber = 3; + std::string tableId = "table id"; + std::string signalId = "signal id"; + std::string memberName = "This is the measured value"; + + nlohmann::json interpretationObject = "just a string"; + + int32_t unitId = 42; + std::string unitDisplayName = "some unit"; + + bsp::SubscribedSignal subscribedSignal(signalNumber, bsp::Logging::logCallback()); + + // some meta information is to be processed to have the signal described: + // -subscribe + // -signal + nlohmann::json subscribeParams; + method = bsp::META_METHOD_SUBSCRIBE; + subscribeParams[bsp::META_SIGNALID] = signalId; + result = subscribedSignal.processSignalMetaInformation(method, subscribeParams); + ASSERT_EQ(result, 0); + + nlohmann::json signalParams; + method = bsp::META_METHOD_SIGNAL; + signalParams[bsp::META_TABLEID] = tableId; + signalParams[bsp::META_DEFINITION][bsp::META_NAME] = memberName; + signalParams[bsp::META_DEFINITION][bsp::META_DATATYPE] = bsp::DATA_TYPE_INT32; + signalParams[bsp::META_DEFINITION][bsp::META_RULE] = bsp::META_RULETYPE_EXPLICIT; + + signalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_UNIT_ID] = unitId; + signalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_DISPLAY_NAME] = unitDisplayName; + signalParams[bsp::META_INTERPRETATION] = interpretationObject; + result = subscribedSignal.processSignalMetaInformation(method, signalParams); + ASSERT_EQ(result, 0); + ASSERT_FALSE(subscribedSignal.isTimeSignal()); + + auto subscribedSignalInfo = SignalDescriptorConverter::ToDataDescriptor(subscribedSignal, NullContext()); + auto dataDescriptor = subscribedSignalInfo.dataDescriptor; + ASSERT_EQ(subscribedSignalInfo.signalName, memberName); + + ASSERT_EQ(dataDescriptor.getSampleType(), daq::SampleType::Int32); + + auto unit = dataDescriptor.getUnit(); + ASSERT_TRUE(unit.assigned()); + ASSERT_EQ(unit.getId(), unitId); + ASSERT_EQ(unit.getSymbol(), unitDisplayName); + + auto rule = dataDescriptor.getRule(); + ASSERT_TRUE(rule.assigned()); + ASSERT_EQ(daq::DataRuleType::Explicit, rule.getType()); + + // test default post scaling + auto postScaling = dataDescriptor.getPostScaling(); + ASSERT_FALSE(postScaling.assigned()); + + // test default range + auto range = dataDescriptor.getValueRange(); + ASSERT_TRUE(range.assigned()); + ASSERT_EQ(range.getLowValue(), std::numeric_limits::lowest()); + ASSERT_EQ(range.getHighValue(), std::numeric_limits::max()); + + // test custom range and post scaling + signalParams[bsp::META_DEFINITION][bsp::META_RANGE][bsp::META_LOW] = -100; + signalParams[bsp::META_DEFINITION][bsp::META_RANGE][bsp::META_HIGH] = 100; + + signalParams[bsp::META_DEFINITION][bsp::META_POSTSCALING][bsp::META_SCALE] = 2.0; + signalParams[bsp::META_DEFINITION][bsp::META_POSTSCALING][bsp::META_POFFSET] = 3.0; + + std::vector < uint8_t > msgpack = nlohmann::json::to_msgpack(signalParams); + nlohmann::json signalParamsToParse = nlohmann::json::from_msgpack(std::vector(msgpack.begin(), msgpack.end())); + + result = subscribedSignal.processSignalMetaInformation(method, signalParamsToParse); + ASSERT_EQ(result, 0); + + subscribedSignalInfo = SignalDescriptorConverter::ToDataDescriptor(subscribedSignal, NullContext()); + dataDescriptor = subscribedSignalInfo.dataDescriptor; + + range = dataDescriptor.getValueRange(); + ASSERT_TRUE(range.assigned()); + ASSERT_EQ(range.getLowValue(), -100); + ASSERT_EQ(range.getHighValue(), 100); + + postScaling = dataDescriptor.getPostScaling(); + ASSERT_TRUE(postScaling.assigned()); + ASSERT_EQ(dataDescriptor.getSampleType(), daq::SampleType::Float64); + ASSERT_EQ(postScaling.getParameters().get("scale"), 2.0); + ASSERT_EQ(postScaling.getParameters().get("offset"), 3.0); +} + +TEST(SignalConverter, subscribedBitfieldSignal) +{ + std::string method; + int result; + unsigned int signalNumber = 3; + std::string tableId = "table id"; + std::string signalId = "signal id"; + std::string memberName = "This is the measured value"; + + nlohmann::json bitsInterpretationObject = + R"([{"Description": "Data overrun","index": 0,"uuid": "c214c128-2447-4cee-ba39-6227aed2eff4"}])"_json; + + bsp::SubscribedSignal subscribedSignal(signalNumber, bsp::Logging::logCallback()); + + // some meta information is to be processed to have the signal described: + // -subscribe + // -signal + nlohmann::json subscribeParams; + method = bsp::META_METHOD_SUBSCRIBE; + subscribeParams[bsp::META_SIGNALID] = signalId; + result = subscribedSignal.processSignalMetaInformation(method, subscribeParams); + ASSERT_EQ(result, 0); + + nlohmann::json signalParams; + method = bsp::META_METHOD_SIGNAL; + signalParams[bsp::META_TABLEID] = tableId; + signalParams[bsp::META_DEFINITION][bsp::META_NAME] = memberName; + signalParams[bsp::META_DEFINITION][bsp::META_DATATYPE] = bsp::DATA_TYPE_BITFIELD; + signalParams[bsp::META_DEFINITION][bsp::DATA_TYPE_BITFIELD]["bits"] = + bitsInterpretationObject; + signalParams[bsp::META_DEFINITION][bsp::DATA_TYPE_BITFIELD][bsp::META_DATATYPE] = + bsp::DATA_TYPE_UINT64; + signalParams[bsp::META_DEFINITION][bsp::META_RULE] = bsp::META_RULETYPE_CONSTANT; + + std::vector < uint8_t > msgpack = nlohmann::json::to_msgpack(signalParams); + nlohmann::json signalParamsToParse = nlohmann::json::from_msgpack(std::vector(msgpack.begin(), msgpack.end())); + + result = subscribedSignal.processSignalMetaInformation(method, signalParamsToParse); + ASSERT_EQ(result, 0); + ASSERT_FALSE(subscribedSignal.isTimeSignal()); + auto subscribedSignalInfo = SignalDescriptorConverter::ToDataDescriptor(subscribedSignal, NullContext()); + auto dataDescriptor = subscribedSignalInfo.dataDescriptor; + ASSERT_EQ(subscribedSignalInfo.signalName, memberName); + + ASSERT_EQ(dataDescriptor.getSampleType(), daq::SampleType::UInt64); + + auto rule = dataDescriptor.getRule(); + ASSERT_TRUE(rule.assigned()); + ASSERT_EQ(daq::DataRuleType::Constant, rule.getType()); + + ASSERT_TRUE(dataDescriptor.getMetadata().hasKey("bits")); + ASSERT_EQ(dataDescriptor.getMetadata().get("bits"), bitsInterpretationObject.dump()); +} + +TEST(SignalConverter, subscribedTimeSignal) +{ + std::string method; + int result; + unsigned int signalNumber = 3; + std::string tableId = "table id"; + std::string signalId = "signal id"; + std::string memberName = "This is the time"; + + uint64_t ticksPerSecond = 10000000; + uint64_t linearDelta = 1000; + int32_t unitId = bsp::Unit::UNIT_ID_SECONDS; + std::string unitDisplayName = "s"; + + bsp::SubscribedSignal subscribedSignal(signalNumber, bsp::Logging::logCallback()); + + // some meta information is to be processed to have the signal described: + // -subscribe + // -signal + // -set the time + nlohmann::json subscribeParams; + method = bsp::META_METHOD_SUBSCRIBE; + + subscribeParams[bsp::META_SIGNALID] = signalId; + result = subscribedSignal.processSignalMetaInformation(method, subscribeParams); + ASSERT_EQ(result, 0); + + nlohmann::json timeSignalParams; + method = bsp::META_METHOD_SIGNAL; + + timeSignalParams[bsp::META_TABLEID] = tableId; + timeSignalParams[bsp::META_DEFINITION][bsp::META_NAME] = memberName; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_RULE] = bsp::META_RULETYPE_LINEAR; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_RULETYPE_LINEAR][bsp::META_DELTA] = linearDelta; + timeSignalParams[bsp::META_DEFINITION][bsp::META_DATATYPE] = bsp::DATA_TYPE_UINT64; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_UNIT_ID] = unitId; + timeSignalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_DISPLAY_NAME] = unitDisplayName; + timeSignalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_QUANTITY] = bsp::META_TIME; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_ABSOLUTE_REFERENCE] = bsp::UNIX_EPOCH; + timeSignalParams[bsp::META_DEFINITION][bsp::META_RESOLUTION][bsp::META_NUMERATOR] = 1; + timeSignalParams[bsp::META_DEFINITION][bsp::META_RESOLUTION][bsp::META_DENOMINATOR] = ticksPerSecond; + + std::vector < uint8_t > msgpack = nlohmann::json::to_msgpack(timeSignalParams); + nlohmann::json timeSignalParamsToParse = nlohmann::json::from_msgpack(std::vector(msgpack.begin(), msgpack.end())); + + result = subscribedSignal.processSignalMetaInformation(method, timeSignalParamsToParse); + ASSERT_EQ(result, 0); + ASSERT_TRUE(subscribedSignal.isTimeSignal()); + + auto subscribedSignalInfo = SignalDescriptorConverter::ToDataDescriptor(subscribedSignal, NullContext()); + auto dataDescriptor = subscribedSignalInfo.dataDescriptor; + ASSERT_EQ(subscribedSignalInfo.signalName, memberName); + + ASSERT_EQ(dataDescriptor.getSampleType(), daq::SampleType::UInt64); + + auto unit = dataDescriptor.getUnit(); + ASSERT_TRUE(unit.assigned()); + ASSERT_EQ(unit.getId(), unitId); + ASSERT_EQ(unit.getSymbol(), unitDisplayName); + + auto rule = dataDescriptor.getRule(); + ASSERT_TRUE(rule.assigned()); + ASSERT_EQ(daq::DataRuleType::Linear, rule.getType()); + DictPtr params = rule.getParameters(); + ASSERT_EQ(params.getCount(), 2u); + uint64_t resultDelta = params.get("delta"); + uint64_t resultStart = params.get("start"); + ASSERT_EQ(resultDelta, linearDelta); + ASSERT_EQ(resultStart, 0); +} + +TEST(SignalConverter, FloatDeltaOfLinearSignal) +{ + std::string method; + int result; + unsigned int signalNumber = 3; + std::string tableId = "table id"; + std::string signalId = "signal id"; + std::string memberName = "This is the time"; + + uint64_t ticksPerSecond = 10000000; + float linearDelta = 4194304.0f; + int32_t unitId = bsp::Unit::UNIT_ID_SECONDS; + std::string unitDisplayName = "s"; + + bsp::SubscribedSignal subscribedSignal(signalNumber, bsp::Logging::logCallback()); + + // some meta information is to be processed to have the signal described: + // -subscribe + // -signal + // -set the time + nlohmann::json subscribeParams; + method = bsp::META_METHOD_SUBSCRIBE; + + subscribeParams[bsp::META_SIGNALID] = signalId; + result = subscribedSignal.processSignalMetaInformation(method, subscribeParams); + ASSERT_EQ(result, 0); + + nlohmann::json timeSignalParams; + method = bsp::META_METHOD_SIGNAL; + + timeSignalParams[bsp::META_TABLEID] = tableId; + timeSignalParams[bsp::META_DEFINITION][bsp::META_NAME] = memberName; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_RULE] = bsp::META_RULETYPE_LINEAR; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_RULETYPE_LINEAR][bsp::META_DELTA] = linearDelta; + timeSignalParams[bsp::META_DEFINITION][bsp::META_DATATYPE] = bsp::DATA_TYPE_REAL32; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_UNIT_ID] = unitId; + timeSignalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_DISPLAY_NAME] = unitDisplayName; + timeSignalParams[bsp::META_DEFINITION][bsp::META_UNIT][bsp::META_QUANTITY] = bsp::META_TIME; + + timeSignalParams[bsp::META_DEFINITION][bsp::META_ABSOLUTE_REFERENCE] = bsp::UNIX_EPOCH; + timeSignalParams[bsp::META_DEFINITION][bsp::META_RESOLUTION][bsp::META_NUMERATOR] = 1; + timeSignalParams[bsp::META_DEFINITION][bsp::META_RESOLUTION][bsp::META_DENOMINATOR] = ticksPerSecond; + + std::vector < uint8_t > msgpack = nlohmann::json::to_msgpack(timeSignalParams); + nlohmann::json timeSignalParamsToParse = nlohmann::json::from_msgpack(std::vector(msgpack.begin(), msgpack.end())); + + result = subscribedSignal.processSignalMetaInformation(method, timeSignalParamsToParse); + ASSERT_EQ(result, 0); + ASSERT_TRUE(subscribedSignal.isTimeSignal()); + + auto subscribedSignalInfo = SignalDescriptorConverter::ToDataDescriptor(subscribedSignal, NullContext()); + auto dataDescriptor = subscribedSignalInfo.dataDescriptor; + ASSERT_EQ(subscribedSignalInfo.signalName, memberName); + + ASSERT_EQ(dataDescriptor.getSampleType(), daq::SampleType::Float32); + + auto unit = dataDescriptor.getUnit(); + ASSERT_TRUE(unit.assigned()); + ASSERT_EQ(unit.getId(), unitId); + ASSERT_EQ(unit.getSymbol(), unitDisplayName); + + auto rule = dataDescriptor.getRule(); + ASSERT_TRUE(rule.assigned()); + ASSERT_EQ(daq::DataRuleType::Linear, rule.getType()); + DictPtr params = rule.getParameters(); + ASSERT_EQ(params.getCount(), 2u); + float resultDelta = params.get("delta"); + uint64_t resultStart = params.get("start"); + ASSERT_EQ(resultDelta, linearDelta); + ASSERT_EQ(resultStart, 0); +} + +END_NAMESPACE_OPENDAQ_WEBSOCKET_STREAMING diff --git a/shared/libraries/websocket_streaming/tests/test_signal_generator.cpp b/shared/libraries/websocket_streaming/tests/test_signal_generator.cpp new file mode 100644 index 0000000..c0f6194 --- /dev/null +++ b/shared/libraries/websocket_streaming/tests/test_signal_generator.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace daq; + +class SignalGeneratorTest : public testing::Test +{ +public: + ContextPtr context; + SignalConfigPtr signal; + SignalGenerator::GenerateSampleFunc stepFunction10; + SignalGenerator::GenerateSampleFunc stepFunction100; + + void SetUp() override + { + context = NullContext(); + signal = createSignal(); + initFunctions(); + } + + void initFunctions() + { + stepFunction10 = [](uint64_t tick, void* sampleOut) + { + int* intOut = (int*) sampleOut; + *intOut = tick % 10; + }; + + stepFunction100 = [](uint64_t tick, void* sampleOut) + { + int* intOut = (int*) sampleOut; + *intOut = tick % 100; + }; + } + + std::vector calculateExpectedSamples(uint64_t startTick, size_t sampleCount, const SignalGenerator::GenerateSampleFunc& function) + { + auto samples = std::vector(sampleCount); + + for (size_t i = 0; i < sampleCount; i++) + function(startTick + i, samples.data() + i); + + return samples; + } + + bool compareSamples(int* expected, void* packetData, size_t sampleCount) + { + return std::memcmp(expected, packetData, sampleCount * sizeof(int)) == 0; + } + +private: + SignalConfigPtr createTimeSignal() + { + const size_t nanosecondsInSecond = 1000000000; + auto delta = nanosecondsInSecond / 1000; + + auto descriptor = DataDescriptorBuilder() + .setSampleType(SampleType::UInt64) + .setRule(LinearDataRule(delta, 0)) + .setTickResolution(Ratio(1, nanosecondsInSecond)) + .setOrigin("1970-01-01T00:00:00") + .setName("Time") + .build(); + + return SignalWithDescriptor(context, descriptor, nullptr, "Time"); + } + + SignalConfigPtr createSignal() + { + auto descriptor = DataDescriptorBuilder().setSampleType(SampleType::Int32).setName("Step").build(); + + auto domainSignal = createTimeSignal(); + auto signal = SignalWithDescriptor(context, descriptor, nullptr, "ByteStep"); + signal.setDomainSignal(domainSignal); + return signal; + } +}; + + +TEST_F(SignalGeneratorTest, CreateSignal) +{ + auto reader = PacketReader(signal); + auto packets = reader.readAll(); + ASSERT_EQ(packets.getCount(), 1u); + ASSERT_EQ(packets[0].getType(), PacketType::Event); + + EventPacketPtr eventPacket = packets[0]; + ASSERT_EQ(eventPacket.getEventId(), event_packet_id::DATA_DESCRIPTOR_CHANGED); +} + +TEST_F(SignalGeneratorTest, StepSignal) +{ + const size_t packetSize = 100; + + auto expectedSamples1 = calculateExpectedSamples(0, packetSize, stepFunction10); + auto expectedSamples2 = calculateExpectedSamples(packetSize, packetSize, stepFunction10); + + auto reader = PacketReader(signal); + + auto generator = SignalGenerator(signal, std::chrono::system_clock::now()); + generator.setFunction(stepFunction10); + generator.generateSamplesTo(std::chrono::milliseconds(packetSize)); + generator.generateSamplesTo(std::chrono::milliseconds(packetSize * 2)); + + auto packets = reader.readAll(); + ASSERT_EQ(packets.getCount(), 3u); + + auto packet1 = packets[1].asPtr(); + ASSERT_EQ(packet1.getSampleCount(), packetSize); + ASSERT_TRUE(compareSamples(expectedSamples1.data(), packet1.getData(), packetSize)); + + auto packet2 = packets[2].asPtr(); + ASSERT_EQ(packet2.getSampleCount(), packetSize); + ASSERT_TRUE(compareSamples(expectedSamples2.data(), packet2.getData(), packetSize)); +} + +TEST_F(SignalGeneratorTest, ChangeFunction) +{ + const size_t packetSize = 100; + + auto expectedSamples1 = calculateExpectedSamples(0, packetSize, stepFunction10); + auto expectedSamples2 = calculateExpectedSamples(packetSize, packetSize, stepFunction100); + + auto reader = PacketReader(signal); + + auto updateFunction = [this](SignalGenerator& generator, uint64_t packetOffset) + { + if (packetOffset > 0) + generator.setFunction(stepFunction100); + }; + + auto generator = SignalGenerator(signal, std::chrono::system_clock::now()); + generator.setFunction(stepFunction10); + generator.setUpdateFunction(updateFunction); + generator.generateSamplesTo(std::chrono::milliseconds(packetSize)); + generator.generateSamplesTo(std::chrono::milliseconds(packetSize * 2)); + + auto packets = reader.readAll(); + ASSERT_EQ(packets.getCount(), 3u); + + auto packet1 = packets[1].asPtr(); + ASSERT_EQ(packet1.getSampleCount(), packetSize); + ASSERT_TRUE(compareSamples(expectedSamples1.data(), packet1.getData(), packetSize)); + + auto packet2 = packets[2].asPtr(); + ASSERT_EQ(packet2.getSampleCount(), packetSize); + ASSERT_TRUE(compareSamples(expectedSamples2.data(), packet2.getData(), packetSize)); +} + +TEST_F(SignalGeneratorTest, SignalGeneratorCountCheck) +{ + const size_t packetSize= 100; + auto expectedSamples1 = calculateExpectedSamples(0, packetSize, stepFunction10); + + auto reader = PacketReader(signal); + + auto generator = SignalGenerator(signal, std::chrono::system_clock::now()); + generator.setFunction(stepFunction10); + generator.generateSamplesTo(std::chrono::milliseconds(packetSize)); + generator.generateSamplesTo(std::chrono::milliseconds(packetSize * 2)); + generator.generateSamplesTo(std::chrono::milliseconds(packetSize * 3)); + + auto packets = reader.readAll(); + ASSERT_EQ(packets.getCount(), 4u); + auto packet1 = packets[1].asPtr(); + ASSERT_EQ(packet1.getSampleCount(), packetSize); + auto packet2 = packets[2].asPtr(); + ASSERT_EQ(packet2.getSampleCount(), packetSize); +} diff --git a/shared/libraries/websocket_streaming/tests/test_streaming.cpp b/shared/libraries/websocket_streaming/tests/test_streaming.cpp new file mode 100644 index 0000000..a0f7700 --- /dev/null +++ b/shared/libraries/websocket_streaming/tests/test_streaming.cpp @@ -0,0 +1,439 @@ +#include +#include +#include + +#include +#include +#include "streaming_test_helpers.h" + + +using namespace daq; +using namespace daq::websocket_streaming; + +class StreamingTest : public testing::Test +{ +public: + const uint16_t StreamingPort = daq::streaming_protocol::WEBSOCKET_LISTENING_PORT; + const std::string StreamingTarget = "/"; + const uint16_t ControlPort = daq::streaming_protocol::HTTP_CONTROL_PORT; + SignalPtr testDoubleSignal; + SignalPtr testConstantSignal; + SignalPtr testDomainSignal; + ContextPtr context; + + Int delta; + Int packetOffset; + + void SetUp() override + { + context = NullContext(); + testDomainSignal = streaming_test_helpers::createLinearTimeSignal(context); + testDoubleSignal = streaming_test_helpers::createExplicitValueSignal(context, "DoubleSignal", testDomainSignal); + testConstantSignal = streaming_test_helpers::createConstantValueSignal(context, "ConstantSignal", testDomainSignal); + + delta = testDomainSignal.getDescriptor().getRule().getParameters().get("delta"); + packetOffset = testDomainSignal.getDescriptor().getRule().getParameters().get("start"); + } + + void TearDown() override + { + } + + DataPacketPtr getNextDomainPacket(size_t sampleCount) + { + auto packet = DataPacket(testDomainSignal.getDescriptor(), sampleCount, packetOffset); + packetOffset += sampleCount * delta; + return packet; + } +}; + +TEST_F(StreamingTest, ConnectAndDisconnect) +{ + auto server = std::make_shared(context); + server->start(StreamingPort, ControlPort); + + auto client = StreamingClient(context, "127.0.0.1", StreamingPort, StreamingTarget); + + ASSERT_FALSE(client.isConnected()); + client.connect(); + + ASSERT_TRUE(client.isConnected()); + + client.disconnect(); + ASSERT_FALSE(client.isConnected()); +} + +TEST_F(StreamingTest, ClientConnectDisconnectCallbacks) +{ + auto server = std::make_shared(context); + + std::string clientId; + bool clientConnected{false}; + server->onClientConnected( + [&clientId, &clientConnected](const std::string& id, const std::string& address) + { + ASSERT_NE(address, ""); + clientConnected = true; + clientId = id; + } + ); + std::promise clientDisconnectedPromise; + std::future clientDisconnectedFuture = clientDisconnectedPromise.get_future(); + server->onClientDisconnected( + [&clientId, &clientConnected, &clientDisconnectedPromise](const std::string& id) + { + if (clientConnected && id == clientId) + clientDisconnectedPromise.set_value(true); + } + ); + + server->start(StreamingPort, ControlPort); + + auto client = std::make_shared(context, "127.0.0.1", StreamingPort, StreamingTarget); + + client->connect(); + ASSERT_TRUE(clientConnected); + ASSERT_NE(clientId, ""); + + client.reset(); + ASSERT_EQ(clientDisconnectedFuture.wait_for(std::chrono::milliseconds(1000)), std::future_status::ready); + ASSERT_TRUE(clientDisconnectedFuture.get()); +} + +TEST_F(StreamingTest, StopServer) +{ + auto server = std::make_shared(context); + server->start(StreamingPort, ControlPort); + + auto client = StreamingClient(context, "127.0.0.1", StreamingPort, StreamingTarget); + + client.connect(); + ASSERT_TRUE(client.isConnected()); + + server->stop(); + server.reset(); +} + +TEST_F(StreamingTest, ConnectTimeout) +{ + auto server = std::make_shared(context); + server->start(StreamingPort, ControlPort); + + auto client = StreamingClient(context, "127.0.0.1", 7000, StreamingTarget); + + client.connect(); + ASSERT_FALSE(client.isConnected()); +} + +TEST_F(StreamingTest, ConnectTwice) +{ + auto server = std::make_shared(context); + server->start(StreamingPort, ControlPort); + + auto client = StreamingClient(context, "127.0.0.1", StreamingPort, StreamingTarget); + + ASSERT_TRUE(client.connect()); + client.disconnect(); + + ASSERT_TRUE(client.connect()); + ASSERT_TRUE(client.isConnected()); + client.disconnect(); + + ASSERT_FALSE(client.isConnected()); +} + +TEST_F(StreamingTest, ParseConnectString) +{ + auto client = std::make_shared(NullContext(), "daq.lt://127.0.0.1"); + ASSERT_EQ(client->getPort(), daq::streaming_protocol::WEBSOCKET_LISTENING_PORT); + ASSERT_EQ(client->getHost(), "127.0.0.1"); + ASSERT_EQ(client->getTarget(), "/"); + + client = std::make_shared(NullContext(), "daq.lt://localhost/path/other"); + ASSERT_EQ(client->getPort(), daq::streaming_protocol::WEBSOCKET_LISTENING_PORT); + ASSERT_EQ(client->getHost(), "localhost"); + ASSERT_EQ(client->getTarget(), "/path/other"); + + client = std::make_shared(NullContext(), "daq.lt://localhost:3000/path/other"); + ASSERT_EQ(client->getPort(), 3000u); + ASSERT_EQ(client->getHost(), "localhost"); + ASSERT_EQ(client->getTarget(), "/path/other"); + + client = std::make_shared(NullContext(), "daq.ws://127.0.0.1"); + ASSERT_EQ(client->getPort(), daq::streaming_protocol::WEBSOCKET_LISTENING_PORT); + ASSERT_EQ(client->getHost(), "127.0.0.1"); + ASSERT_EQ(client->getTarget(), "/"); +} + +TEST_F(StreamingTest, Subscription) +{ + SKIP_TEST_MAC_CI; + auto server = std::make_shared(context); + server->onAccept([this](const daq::streaming_protocol::StreamWriterPtr& writer) { + auto signals = List(); + signals.pushBack(testDoubleSignal); + signals.pushBack(testDoubleSignal.getDomainSignal()); + return signals; + }); + server->start(StreamingPort, ControlPort); + + auto client = StreamingClient(context, "127.0.0.1", StreamingPort, StreamingTarget); + + std::promise subscribeAckPromise; + std::future subscribeAckFuture = subscribeAckPromise.get_future(); + + std::promise unsubscribeAckPromise; + std::future unsubscribeAckFuture = unsubscribeAckPromise.get_future(); + + auto onSubscriptionAck = + [&subscribeAckPromise, &unsubscribeAckPromise](const std::string& signalId, bool subscribed) + { + if (subscribed) + subscribeAckPromise.set_value(signalId); + else + unsubscribeAckPromise.set_value(signalId); + }; + + client.onSubscriptionAck(onSubscriptionAck); + client.connect(); + ASSERT_TRUE(client.isConnected()); + + client.subscribeSignal(testDoubleSignal.getGlobalId()); + ASSERT_EQ(subscribeAckFuture.wait_for(std::chrono::milliseconds(1000)), std::future_status::ready); + ASSERT_EQ(subscribeAckFuture.get(), testDoubleSignal.getGlobalId()); + + client.unsubscribeSignal(testDoubleSignal.getGlobalId()); + ASSERT_EQ(unsubscribeAckFuture.wait_for(std::chrono::milliseconds(1000)), std::future_status::ready); + ASSERT_EQ(unsubscribeAckFuture.get(), testDoubleSignal.getGlobalId()); +} + +// sends explicit value packet after constant value packet +TEST_F(StreamingTest, PacketsCorrectSequence) +{ + SKIP_TEST_MAC_CI; + std::vector data = {-1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5}; + auto sampleCount = data.size(); + + auto server = std::make_shared(context); + server->onAccept([this](const daq::streaming_protocol::StreamWriterPtr& writer) { + auto signals = List(); + signals.pushBack(testDomainSignal); + signals.pushBack(testDoubleSignal); + signals.pushBack(testConstantSignal); + return signals; + }); + server->start(StreamingPort, ControlPort); + + std::vector receivedPackets; + auto client = StreamingClient(context, "127.0.0.1", StreamingPort, StreamingTarget); + + std::map> subscribeAckPromises; + subscribeAckPromises.emplace(testConstantSignal.getGlobalId(), std::promise()); + subscribeAckPromises.emplace(testDoubleSignal.getGlobalId(), std::promise()); + + std::map> subscribeAckFutures; + subscribeAckFutures.emplace(testConstantSignal.getGlobalId(), subscribeAckPromises.at(testConstantSignal.getGlobalId()).get_future()); + subscribeAckFutures.emplace(testDoubleSignal.getGlobalId(), subscribeAckPromises.at(testDoubleSignal.getGlobalId()).get_future()); + + auto onSubscriptionAck = + [&subscribeAckPromises](const std::string& signalId, bool subscribed) + { + ASSERT_EQ(subscribeAckPromises.count(signalId), 1u); + if (subscribed) + subscribeAckPromises.at(signalId).set_value(); + }; + + auto onPacket = [&receivedPackets](const StringPtr& signalId, const PacketPtr& packet) + { + receivedPackets.push_back(packet); + }; + + client.onPacket(onPacket); + client.onSubscriptionAck(onSubscriptionAck); + client.connect(); + ASSERT_TRUE(client.isConnected()); + + client.subscribeSignal(testConstantSignal.getGlobalId()); + client.subscribeSignal(testDoubleSignal.getGlobalId()); + + ASSERT_EQ(subscribeAckFutures.at(testConstantSignal.getGlobalId()).wait_for(std::chrono::seconds(5)), std::future_status::ready); + ASSERT_EQ(subscribeAckFutures.at(testDoubleSignal.getGlobalId()).wait_for(std::chrono::seconds(5)), std::future_status::ready); + + auto domainPacket1 = getNextDomainPacket(sampleCount); + auto explicitValuePacket1 = DataPacketWithDomain(domainPacket1, testDoubleSignal.getDescriptor(), sampleCount); + std::memcpy(explicitValuePacket1.getRawData(), data.data(), explicitValuePacket1.getRawDataSize()); + auto constantValuePacket1 = ConstantDataPacketWithDomain(domainPacket1, + testConstantSignal.getDescriptor(), + sampleCount, + 1, + {{2, 2}, {4, 4}, {6, 5}}); + + server->broadcastPacket(testConstantSignal.getGlobalId(), constantValuePacket1); + server->broadcastPacket(testDoubleSignal.getGlobalId(), explicitValuePacket1); + + auto domainPacket2 = getNextDomainPacket(sampleCount); + auto explicitValuePacket2 = DataPacketWithDomain(domainPacket2, testDoubleSignal.getDescriptor(), sampleCount); + std::memcpy(explicitValuePacket2.getRawData(), data.data(), explicitValuePacket2.getRawDataSize()); + + server->broadcastPacket(testDoubleSignal.getGlobalId(), explicitValuePacket2); + + auto domainPacket3 = getNextDomainPacket(sampleCount); + auto explicitValuePacket3 = DataPacketWithDomain(domainPacket3, testDoubleSignal.getDescriptor(), sampleCount); + std::memcpy(explicitValuePacket3.getRawData(), data.data(), explicitValuePacket3.getRawDataSize()); + auto constantValuePacket3 = ConstantDataPacketWithDomain(domainPacket3, + testConstantSignal.getDescriptor(), + sampleCount, + 1); + + server->broadcastPacket(testConstantSignal.getGlobalId(), constantValuePacket3); + server->broadcastPacket(testDoubleSignal.getGlobalId(), explicitValuePacket3); + + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // 2 event packets + data packets: 3 domain and 6 value packets + ASSERT_EQ(receivedPackets.size(), 11u); + + ASSERT_TRUE(BaseObjectPtr::Equals(domainPacket1, receivedPackets[2])); + ASSERT_TRUE(BaseObjectPtr::Equals(explicitValuePacket1, receivedPackets[3])); + ASSERT_TRUE(BaseObjectPtr::Equals(constantValuePacket1, receivedPackets[4])); + + // packet automatically generated by client + auto constantValuePacket2 = ConstantDataPacketWithDomain(domainPacket2, + testConstantSignal.getDescriptor(), + sampleCount, + 5); + + ASSERT_TRUE(BaseObjectPtr::Equals(domainPacket2, receivedPackets[5])); + ASSERT_TRUE(BaseObjectPtr::Equals(explicitValuePacket2, receivedPackets[6])); + ASSERT_TRUE(BaseObjectPtr::Equals(constantValuePacket2, receivedPackets[7])); + + ASSERT_TRUE(BaseObjectPtr::Equals(domainPacket3, receivedPackets[8])); + ASSERT_TRUE(BaseObjectPtr::Equals(explicitValuePacket3, receivedPackets[9])); + ASSERT_TRUE(BaseObjectPtr::Equals(constantValuePacket3, receivedPackets[10])); +} + +// sends explicit value packet before constant value packet +// this results in client side doesn't yet have appropriate data values to generate constant packet +// so it uses the default value instead. +TEST_F(StreamingTest, PacketsIncorrectSequence) +{ + SKIP_TEST_MAC_CI; + std::vector data = {-1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5}; + auto sampleCount = data.size(); + + uint64_t defaultStartValue = 0x0; + auto constantDefaultValuePacket = ConstantDataPacketWithDomain(getNextDomainPacket(sampleCount), + testConstantSignal.getDescriptor(), + sampleCount, + defaultStartValue); + // init last value to be used as default start value of signal + testConstantSignal.asPtr().sendPacket(constantDefaultValuePacket); + + auto server = std::make_shared(context); + server->onAccept([this](const daq::streaming_protocol::StreamWriterPtr& writer) { + auto signals = List(); + signals.pushBack(testDomainSignal); + signals.pushBack(testDoubleSignal); + signals.pushBack(testConstantSignal); + return signals; + }); + server->start(StreamingPort, ControlPort); + + std::vector receivedPackets; + auto client = StreamingClient(context, "127.0.0.1", StreamingPort, StreamingTarget); + + std::map> subscribeAckPromises; + subscribeAckPromises.emplace(testConstantSignal.getGlobalId(), std::promise()); + subscribeAckPromises.emplace(testDoubleSignal.getGlobalId(), std::promise()); + + std::map> subscribeAckFutures; + subscribeAckFutures.emplace(testConstantSignal.getGlobalId(), subscribeAckPromises.at(testConstantSignal.getGlobalId()).get_future()); + subscribeAckFutures.emplace(testDoubleSignal.getGlobalId(), subscribeAckPromises.at(testDoubleSignal.getGlobalId()).get_future()); + + auto onSubscriptionAck = + [&subscribeAckPromises](const std::string& signalId, bool subscribed) + { + ASSERT_EQ(subscribeAckPromises.count(signalId), 1u); + if (subscribed) + subscribeAckPromises.at(signalId).set_value(); + }; + + auto onPacket = [&receivedPackets](const StringPtr& signalId, const PacketPtr& packet) + { + receivedPackets.push_back(packet); + }; + + client.onPacket(onPacket); + client.onSubscriptionAck(onSubscriptionAck); + client.connect(); + ASSERT_TRUE(client.isConnected()); + + client.subscribeSignal(testConstantSignal.getGlobalId()); + client.subscribeSignal(testDoubleSignal.getGlobalId()); + + ASSERT_EQ(subscribeAckFutures.at(testConstantSignal.getGlobalId()).wait_for(std::chrono::seconds(5)), std::future_status::ready); + ASSERT_EQ(subscribeAckFutures.at(testDoubleSignal.getGlobalId()).wait_for(std::chrono::seconds(5)), std::future_status::ready); + + auto domainPacket1 = getNextDomainPacket(sampleCount); + auto explicitValuePacket1 = DataPacketWithDomain(domainPacket1, testDoubleSignal.getDescriptor(), sampleCount); + std::memcpy(explicitValuePacket1.getRawData(), data.data(), explicitValuePacket1.getRawDataSize()); + auto constantValuePacket1 = ConstantDataPacketWithDomain(domainPacket1, + testConstantSignal.getDescriptor(), + sampleCount, + 1, + {{2, 2}, {4, 4}, {6, 5}}); + + server->broadcastPacket(testDoubleSignal.getGlobalId(), explicitValuePacket1); + server->broadcastPacket(testConstantSignal.getGlobalId(), constantValuePacket1); + + auto domainPacket2 = getNextDomainPacket(sampleCount); + auto explicitValuePacket2 = DataPacketWithDomain(domainPacket2, testDoubleSignal.getDescriptor(), sampleCount); + std::memcpy(explicitValuePacket2.getRawData(), data.data(), explicitValuePacket2.getRawDataSize()); + + server->broadcastPacket(testDoubleSignal.getGlobalId(), explicitValuePacket2); + + auto domainPacket3 = getNextDomainPacket(sampleCount); + auto explicitValuePacket3 = DataPacketWithDomain(domainPacket3, testDoubleSignal.getDescriptor(), sampleCount); + std::memcpy(explicitValuePacket3.getRawData(), data.data(), explicitValuePacket3.getRawDataSize()); + auto constantValuePacket3 = ConstantDataPacketWithDomain(domainPacket3, + testConstantSignal.getDescriptor(), + sampleCount, + 1); + + server->broadcastPacket(testDoubleSignal.getGlobalId(), explicitValuePacket3); + server->broadcastPacket(testConstantSignal.getGlobalId(), constantValuePacket3); + + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // packets automatically generated by client + auto clientConstValuePacket1 = ConstantDataPacketWithDomain(domainPacket1, + testConstantSignal.getDescriptor(), + sampleCount, + defaultStartValue); + auto clientConstValuePacket2 = ConstantDataPacketWithDomain(domainPacket2, + testConstantSignal.getDescriptor(), + sampleCount, + 5); + auto clientConstValuePacket3 = ConstantDataPacketWithDomain(domainPacket3, + testConstantSignal.getDescriptor(), + sampleCount, + 5); + + // 2 event packets + data packets: 3 domain and 6 value packets + ASSERT_EQ(receivedPackets.size(), 11u); + + ASSERT_TRUE(BaseObjectPtr::Equals(domainPacket1, receivedPackets[2])); + ASSERT_TRUE(BaseObjectPtr::Equals(explicitValuePacket1, receivedPackets[3])); + // constant packet with default value generated since the signal data values are unknown + ASSERT_TRUE(BaseObjectPtr::Equals(clientConstValuePacket1, receivedPackets[4])); + + ASSERT_TRUE(BaseObjectPtr::Equals(domainPacket2, receivedPackets[5])); + ASSERT_TRUE(BaseObjectPtr::Equals(explicitValuePacket2, receivedPackets[6])); + // contains only last change of constant + ASSERT_TRUE(BaseObjectPtr::Equals(clientConstValuePacket2, receivedPackets[7])); + + ASSERT_TRUE(BaseObjectPtr::Equals(domainPacket3, receivedPackets[8])); + ASSERT_TRUE(BaseObjectPtr::Equals(explicitValuePacket3, receivedPackets[9])); + // value update is not yet applied, provides old value + ASSERT_TRUE(BaseObjectPtr::Equals(clientConstValuePacket3, receivedPackets[10])); +} diff --git a/shared/libraries/websocket_streaming/tests/test_websocket_client_device.cpp b/shared/libraries/websocket_streaming/tests/test_websocket_client_device.cpp new file mode 100644 index 0000000..6d00ead --- /dev/null +++ b/shared/libraries/websocket_streaming/tests/test_websocket_client_device.cpp @@ -0,0 +1,761 @@ +#include +#include +#include +#include +#include +#include +#include +#include "streaming_test_helpers.h" +#include +#include + +using namespace daq; +using namespace std::chrono_literals; +using namespace daq::websocket_streaming; + +class WebsocketClientDeviceTest : public testing::Test +{ +public: + const uint16_t STREAMING_PORT = daq::streaming_protocol::WEBSOCKET_LISTENING_PORT; + const uint16_t CONTROL_PORT = daq::streaming_protocol::HTTP_CONTROL_PORT; + const std::string HOST = "127.0.0.1"; + ContextPtr context; + + void SetUp() override + { + context = NullContext(); + } + + void TearDown() override + { + } +}; + +TEST_F(WebsocketClientDeviceTest, CreateWithInvalidParameters) +{ + ASSERT_THROW(WebsocketClientDevice(context, nullptr, "device", nullptr), ArgumentNullException); +} + +TEST_F(WebsocketClientDeviceTest, CreateSuccess) +{ + // Create server side device + auto serverInstance = streaming_test_helpers::createServerInstance(); + + // Setup and start streaming server + auto server = WebsocketStreamingServer(serverInstance); + server.setStreamingPort(STREAMING_PORT); + server.setControlPort(CONTROL_PORT); + server.start(); + + DevicePtr clientDevice; + ASSERT_NO_THROW(clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST)); +} + +TEST_F(WebsocketClientDeviceTest, DeviceInfo) +{ + // Create server side device + auto serverInstance = streaming_test_helpers::createServerInstance(); + + // Setup and start streaming server + auto server = WebsocketStreamingServer(serverInstance); + server.setStreamingPort(STREAMING_PORT); + server.setControlPort(CONTROL_PORT); + server.start(); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + + // get DeviceInfo and check fields + DeviceInfoPtr clientDeviceInfo; + ASSERT_NO_THROW(clientDeviceInfo = clientDevice.getInfo()); + ASSERT_EQ(clientDeviceInfo.getName(), "WebsocketClientPseudoDevice"); + ASSERT_EQ(clientDeviceInfo.getConnectionString(), HOST); +} + +class WebsocketClientDeviceTestP : public WebsocketClientDeviceTest, public testing::WithParamInterface +{ +public: + bool addSignals(const ListPtr& signals, + const StreamingServerPtr& server, + const DevicePtr& device) + { + SizeT addedSigCount = 0; + std::promise addSigPromise; + std::future addSigFuture = addSigPromise.get_future(); + + auto eventHandler = [&](ComponentPtr comp, CoreEventArgsPtr args) + { + auto params = args.getParameters(); + if (static_cast(args.getEventId()) == CoreEventId::ComponentAdded) + { + ComponentPtr component = params.get("Component"); + if (component.supportsInterface()) + { + addedSigCount++; + if (addedSigCount == signals.getCount()) + addSigPromise.set_value(); + } + } + }; + + device.getItem("Sig").getOnComponentCoreEvent() += eventHandler; + + server->addSignals(signals); + + bool result = (addSigFuture.wait_for(std::chrono::seconds(5)) == std::future_status::ready); + + device.getItem("Sig").getOnComponentCoreEvent() -= eventHandler; + return result; + } + + bool removeSignals(const ListPtr& signals, + const StreamingServerPtr& server, + const DevicePtr& device) + { + SizeT removedSigCount = 0; + std::promise rmSigPromise; + std::future rmSigFuture = rmSigPromise.get_future(); + + auto eventHandler = [&](ComponentPtr comp, CoreEventArgsPtr args) + { + if (static_cast(args.getEventId()) == CoreEventId::ComponentRemoved) + { + removedSigCount++; + if (removedSigCount == signals.getCount()) + rmSigPromise.set_value(); + } + }; + + device.getItem("Sig").getOnComponentCoreEvent() += eventHandler; + + for (const auto& signal : signals) + server->removeComponentSignals(signal.getGlobalId()); + + bool result = (rmSigFuture.wait_for(std::chrono::seconds(5)) == std::future_status::ready); + + device.getItem("Sig").getOnComponentCoreEvent() -= eventHandler; + return result; + } +}; + +TEST_P(WebsocketClientDeviceTestP, SignalWithDomain) +{ + const bool signalsAddedAfterConnect = GetParam(); + + // Create server signals + auto testDomainSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto testValueSignal = streaming_test_helpers::createExplicitValueSignal(context, "TestName", testDomainSignal); + auto signals = List(testValueSignal, testDomainSignal); + + // Setup and start server which will publish created signal + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { + if (signalsAddedAfterConnect) + return List(); + else + return signals; + }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + + if (signalsAddedAfterConnect) + { + ASSERT_TRUE(addSignals(signals, server, clientDevice)); + } + + // Check the mirrored signal + ASSERT_EQ(clientDevice.getSignals().getCount(), 2u); + ASSERT_TRUE(clientDevice.getSignals()[0].getDescriptor().assigned()); + ASSERT_TRUE(clientDevice.getSignals()[0].getDomainSignal().assigned()); + + ASSERT_FALSE(clientDevice.getSignals()[1].getDomainSignal().assigned()); + ASSERT_EQ(clientDevice.getSignals()[0].getDomainSignal(), clientDevice.getSignals()[1]); + + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[0].getDescriptor(), + testValueSignal.getDescriptor())); + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[0].getDomainSignal().getDescriptor(), + testValueSignal.getDomainSignal().getDescriptor())); + + ASSERT_EQ(clientDevice.getSignals()[0].getName(), "TestName"); + ASSERT_EQ(clientDevice.getSignals()[0].getDescription(), "TestDescription"); + + std::promise acknowledgementPromise; + std::future acknowledgementFuture = acknowledgementPromise.get_future(); + auto signal = clientDevice.getSignals()[0].asPtr(); + signal.getOnSubscribeComplete() += + [&acknowledgementPromise](MirroredSignalConfigPtr&, SubscriptionEventArgsPtr& args) + { + acknowledgementPromise.set_value(args.getStreamingConnectionString()); + }; + + auto reader = PacketReader(clientDevice.getSignals()[0]); + ASSERT_EQ(acknowledgementFuture.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + // Publish signal changes + auto descriptor = DataDescriptorBuilderCopy(testValueSignal.getDescriptor()).build(); + std::string signalId = testValueSignal.getGlobalId(); + server->broadcastPacket(signalId, DataDescriptorChangedEventPacket(descriptor, testValueSignal.getDomainSignal().getDescriptor())); + + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + testValueSignal.asPtr().unlockAllAttributes(); + testValueSignal.setName("NewName"); + testValueSignal.setDescription("NewDescription"); + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + ASSERT_EQ(clientDevice.getSignals()[0].getName(), "NewName"); + ASSERT_EQ(clientDevice.getSignals()[0].getDescription(), "NewDescription"); + + ASSERT_NO_THROW(clientDevice.getSignals()[0].setName("ClientName")); + + // Check if the mirrored signal changed + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[0].getDescriptor(), + descriptor)); + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[0].getDomainSignal().getDescriptor(), + testValueSignal.getDomainSignal().getDescriptor())); + + ASSERT_TRUE(removeSignals({testDomainSignal}, server, clientDevice)); + ASSERT_EQ(clientDevice.getSignals().getCount(), 1u); + ASSERT_FALSE(clientDevice.getSignals()[0].getDomainSignal().assigned()); + + ASSERT_TRUE(removeSignals({testValueSignal}, server, clientDevice)); + ASSERT_EQ(clientDevice.getSignals().getCount(), 0u); +} + +TEST_P(WebsocketClientDeviceTestP, SingleDomainSignal) +{ + const bool signalsAddedAfterConnect = GetParam(); + + // Create server signal + auto testSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto signals = List(testSignal); + + // Setup and start server which will publish created signal + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { + if (signalsAddedAfterConnect) + return List(); + else + return signals; + }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + + if (signalsAddedAfterConnect) + { + ASSERT_TRUE(addSignals(signals, server, clientDevice)); + } + + // The mirrored signal exists and has descriptor + ASSERT_EQ(clientDevice.getSignals().getCount(), 1u); + ASSERT_TRUE(clientDevice.getSignals()[0].getDescriptor().assigned()); + ASSERT_FALSE(clientDevice.getSignals()[0].getDomainSignal().assigned()); + + ASSERT_TRUE(removeSignals(signals, server, clientDevice)); + ASSERT_EQ(clientDevice.getSignals().getCount(), 0u); +} + +TEST_P(WebsocketClientDeviceTestP, SingleUnsupportedSignal) +{ + const bool signalsAddedAfterConnect = GetParam(); + + // Create server signal + auto testSignal = streaming_test_helpers::createExplicitValueSignal(context, "TestSignal", nullptr); + auto signals = List(testSignal); + + // Setup and start server which will publish created signal + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { + if (signalsAddedAfterConnect) + return List(); + else + return signals; + }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + + if (signalsAddedAfterConnect) + { + ASSERT_TRUE(addSignals(signals, server, clientDevice)); + } + + // The mirrored signal exists but does not have descriptor + ASSERT_EQ(clientDevice.getSignals().getCount(), 1u); + ASSERT_FALSE(clientDevice.getSignals()[0].getDescriptor().assigned()); + ASSERT_FALSE(clientDevice.getSignals()[0].getDomainSignal().assigned()); + + ASSERT_TRUE(removeSignals(signals, server, clientDevice)); + ASSERT_EQ(clientDevice.getSignals().getCount(), 0u); +} + +TEST_P(WebsocketClientDeviceTestP, SignalsWithSharedDomain) +{ + const bool signalsAddedAfterConnect = GetParam(); + + // Create server signals + auto timeSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto dataSignal1 = streaming_test_helpers::createExplicitValueSignal(context, "Data1", timeSignal); + auto dataSignal2 = streaming_test_helpers::createExplicitValueSignal(context, "Data2", timeSignal); + auto signals = List(dataSignal1, timeSignal, dataSignal2); + + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { + if (signalsAddedAfterConnect) + return List(); + else + return signals; + }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + + if (signalsAddedAfterConnect) + { + ASSERT_TRUE(addSignals(signals, server, clientDevice)); + } + + ASSERT_EQ(clientDevice.getSignals().getCount(), 3u); + + // Check the mirrored signals + ASSERT_TRUE(clientDevice.getSignals()[0].getDescriptor().assigned()); + ASSERT_TRUE(clientDevice.getSignals()[0].getDomainSignal().assigned()); + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[0].getDescriptor(), + dataSignal1.getDescriptor())); + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[0].getDomainSignal().getDescriptor(), + dataSignal1.getDomainSignal().getDescriptor())); + ASSERT_EQ(clientDevice.getSignals()[0].getName(), "Data1"); + + ASSERT_TRUE(clientDevice.getSignals()[1].getDescriptor().assigned()); + ASSERT_TRUE(!clientDevice.getSignals()[1].getDomainSignal().assigned()); + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[1].getDescriptor(), + timeSignal.getDescriptor())); + ASSERT_EQ(clientDevice.getSignals()[1].getName(), "Time"); + + ASSERT_TRUE(clientDevice.getSignals()[2].getDescriptor().assigned()); + ASSERT_TRUE(clientDevice.getSignals()[2].getDomainSignal().assigned()); + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[2].getDescriptor(), + dataSignal2.getDescriptor())); + ASSERT_TRUE(BaseObjectPtr::Equals(clientDevice.getSignals()[2].getDomainSignal().getDescriptor(), + dataSignal2.getDomainSignal().getDescriptor())); + ASSERT_EQ(clientDevice.getSignals()[2].getName(), "Data2"); + + ASSERT_EQ(clientDevice.getSignals()[2].getDomainSignal(), clientDevice.getSignals()[1]); + ASSERT_EQ(clientDevice.getSignals()[0].getDomainSignal(), clientDevice.getSignals()[1]); + + ASSERT_TRUE(removeSignals({timeSignal}, server, clientDevice)); + ASSERT_EQ(clientDevice.getSignals().getCount(), 2u); + ASSERT_FALSE(clientDevice.getSignals()[0].getDomainSignal().assigned()); + ASSERT_FALSE(clientDevice.getSignals()[1].getDomainSignal().assigned()); + + ASSERT_TRUE(removeSignals({dataSignal1, dataSignal2}, server, clientDevice)); + ASSERT_EQ(clientDevice.getSignals().getCount(), 0u); +} + +INSTANTIATE_TEST_SUITE_P(SignalsAddedAfterConnect, WebsocketClientDeviceTestP, testing::Values(true, false)); + +TEST_F(WebsocketClientDeviceTest, ChangeValueDescriptorToSupported) +{ + SKIP_TEST_MAC_CI; + // Create server signals + auto testDomainSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto testValueSignal = streaming_test_helpers::createExplicitValueSignal(context, "TestName", testDomainSignal); + auto signals = List(testValueSignal, testDomainSignal); + + // Setup and start server which will publish created signals + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { return signals; }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + auto valueSignal = clientDevice.getSignals()[0].asPtr(); + + // subscribe value signal + std::promise acknowledgementPromise; + std::future acknowledgementFuture = acknowledgementPromise.get_future(); + valueSignal.getOnSubscribeComplete() += + [&acknowledgementPromise](MirroredSignalConfigPtr&, SubscriptionEventArgsPtr& args) + { + try + { + acknowledgementPromise.set_value(args.getStreamingConnectionString()); + } + catch(const std::exception& e) + { + FAIL() << e.what(); + } + }; + auto reader = PacketReader(valueSignal); + ASSERT_EQ(acknowledgementFuture.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + SizeT addedSigCount = 0; + std::promise addSigPromise; + std::future addSigFuture = addSigPromise.get_future(); + auto eventHandler = [&](const ComponentPtr& comp, const CoreEventArgsPtr& args) + { + auto params = args.getParameters(); + if (static_cast(args.getEventId()) == CoreEventId::ComponentAdded && + params.get("Component").supportsInterface() && + ++addedSigCount == 1) + { + addSigPromise.set_value(); + } + }; + clientDevice.getContext().getOnCoreEvent() += eventHandler; + + // remove post scaling to change output sampletype + const auto supportedValueDescriptor = + DataDescriptorBuilderCopy(testValueSignal.getDescriptor()).setPostScaling(nullptr).build(); + testValueSignal.asPtr().setDescriptor(supportedValueDescriptor); + server->broadcastPacket( + testValueSignal.getGlobalId(), + DataDescriptorChangedEventPacket(supportedValueDescriptor, nullptr) + ); + + // wait for 1 new signal added + ASSERT_TRUE(addSigFuture.wait_for(std::chrono::seconds(5)) == std::future_status::ready); + clientDevice.getContext().getOnCoreEvent() -= eventHandler; + + // Check if old value signal removed + ASSERT_TRUE(valueSignal.isRemoved()); + + ASSERT_EQ(clientDevice.getSignals().getCount(), 2u); + valueSignal = clientDevice.getSignals()[1].asPtr(); + + // wait for subscribe ack + ASSERT_EQ(acknowledgementFuture.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + // wait for new descriptors assigned + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // Check the updated mirrored signal + ASSERT_TRUE(valueSignal.getDescriptor().assigned()); + ASSERT_EQ(valueSignal.getDomainSignal(), clientDevice.getSignals()[0]); + ASSERT_TRUE(BaseObjectPtr::Equals(valueSignal.getDescriptor(), supportedValueDescriptor)); +} + +class UnsupportedSignalsTestP : public WebsocketClientDeviceTest, public testing::WithParamInterface +{ +public: + void SetUp() override + { + unsupportedDescriptor = GetParam(); + context = NullContext(); + } + + DataDescriptorPtr unsupportedDescriptor; +}; + +TEST_P(UnsupportedSignalsTestP, MakeValueSignalUnsupported) +{ + SKIP_TEST_MAC_CI; + // Create server signals + auto testDomainSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto testValueSignal = streaming_test_helpers::createExplicitValueSignal(context, "TestName", testDomainSignal); + auto signals = List(testValueSignal, testDomainSignal); + + // Setup and start server which will publish created signals + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { return signals; }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + auto valueSignal = clientDevice.getSignals()[0].asPtr(); + + // subscribe value signal + std::promise acknowledgementPromise; + std::future acknowledgementFuture = acknowledgementPromise.get_future(); + valueSignal.getOnSubscribeComplete() += + [&acknowledgementPromise](MirroredSignalConfigPtr&, SubscriptionEventArgsPtr& args) + { + acknowledgementPromise.set_value(args.getStreamingConnectionString()); + }; + auto reader = PacketReader(valueSignal); + ASSERT_EQ(acknowledgementFuture.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + SizeT addedSigCount = 0; + std::promise addSigPromise; + std::future addSigFuture = addSigPromise.get_future(); + + auto eventHandler = [&](const ComponentPtr& comp, const CoreEventArgsPtr& args) + { + auto params = args.getParameters(); + if (static_cast(args.getEventId()) == CoreEventId::ComponentAdded && + params.get("Component").supportsInterface() && + ++addedSigCount == 1) + { + addSigPromise.set_value(); + } + }; + clientDevice.getContext().getOnCoreEvent() += eventHandler; + + // make value signal unsupported + testValueSignal.asPtr().setDescriptor(unsupportedDescriptor); + server->broadcastPacket( + testValueSignal.getGlobalId(), + DataDescriptorChangedEventPacket(descriptorToEventPacketParam(unsupportedDescriptor), nullptr) + ); + + // wait for 1 new incomplete signal added + ASSERT_TRUE(addSigFuture.wait_for(std::chrono::seconds(5)) == std::future_status::ready); + clientDevice.getContext().getOnCoreEvent() -= eventHandler; + + // Check if old value signal removed + ASSERT_TRUE(valueSignal.isRemoved()); + + ASSERT_EQ(clientDevice.getSignals().getCount(), 2u); + valueSignal = clientDevice.getSignals()[1].asPtr(); + auto domainSignal = clientDevice.getSignals()[0].asPtr(); + + ASSERT_TRUE(domainSignal.getDescriptor().assigned()); + // Check if new value signal has nullptr descriptors + ASSERT_FALSE(valueSignal.getDescriptor().assigned()); +} + +TEST_P(UnsupportedSignalsTestP, MakeDomainSignalUnsupported) +{ + SKIP_TEST_MAC_CI; + // Create server signals + auto testDomainSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto testValueSignal = streaming_test_helpers::createExplicitValueSignal(context, "TestName", testDomainSignal); + auto signals = List(testValueSignal, testDomainSignal); + + // Setup and start server which will publish created signals + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { return signals; }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + auto valueSignal = clientDevice.getSignals()[0].asPtr(); + auto domainSignal = clientDevice.getSignals()[1].asPtr(); + + // subscribe value signal + std::promise acknowledgementPromise; + std::future acknowledgementFuture = acknowledgementPromise.get_future(); + valueSignal.getOnSubscribeComplete() += + [&acknowledgementPromise](MirroredSignalConfigPtr&, SubscriptionEventArgsPtr& args) + { + acknowledgementPromise.set_value(args.getStreamingConnectionString()); + }; + auto reader = PacketReader(valueSignal); + ASSERT_EQ(acknowledgementFuture.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + SizeT addedSigCount = 0; + std::promise addSigPromise; + std::future addSigFuture = addSigPromise.get_future(); + auto eventHandler = [&](const ComponentPtr& comp, const CoreEventArgsPtr& args) + { + auto params = args.getParameters(); + if (static_cast(args.getEventId()) == CoreEventId::ComponentAdded && + params.get("Component").supportsInterface() && + ++addedSigCount == 2) + { + addSigPromise.set_value(); + } + }; + clientDevice.getContext().getOnCoreEvent() += eventHandler; + + // make domain signal unsupported + testDomainSignal.asPtr().setDescriptor(unsupportedDescriptor); + server->broadcastPacket( + testValueSignal.getGlobalId(), + DataDescriptorChangedEventPacket(nullptr, descriptorToEventPacketParam(unsupportedDescriptor)) + ); + + // wait for 2 new incomplete signals added + ASSERT_TRUE(addSigFuture.wait_for(std::chrono::seconds(5)) == std::future_status::ready); + clientDevice.getContext().getOnCoreEvent() -= eventHandler; + + // Check if old signals removed + ASSERT_TRUE(valueSignal.isRemoved()); + ASSERT_TRUE(domainSignal.isRemoved()); + + ASSERT_EQ(clientDevice.getSignals().getCount(), 2u); + valueSignal = clientDevice.getSignals()[0]; + domainSignal = clientDevice.getSignals()[1]; + + // Check if new signals have nullptr descriptors + ASSERT_FALSE(valueSignal.getDescriptor().assigned()); + ASSERT_FALSE(domainSignal.getDescriptor().assigned()); +} + +TEST_P(UnsupportedSignalsTestP, MakeValueSignalSupported) +{ + SKIP_TEST_MAC_CI; + // Create server signals + auto testDomainSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto testValueSignal = streaming_test_helpers::createExplicitValueSignal(context, "TestName", testDomainSignal); + auto signals = List(testValueSignal, testDomainSignal); + + const auto supportedValueDescriptor = DataDescriptorBuilderCopy(testValueSignal.getDescriptor()).build(); + // Set unsupported descriptor + testValueSignal.asPtr().setDescriptor(unsupportedDescriptor); + + // Setup and start server which will publish created signals + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { return signals; }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + + ASSERT_EQ(clientDevice.getSignals().getCount(), 2u); + auto valueSignal = clientDevice.getSignals()[0].asPtr(); + auto domainSignal = clientDevice.getSignals()[1].asPtr(); + ASSERT_FALSE(valueSignal.getDescriptor().assigned()); + ASSERT_TRUE(domainSignal.getDescriptor().assigned()); + + // Wait for the signals temporarily subscribed by the client during initialization to be unsubscribed, + // to avoid interfering with the explicit subscription triggered by reader creation. + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // subscribe value signal + std::promise acknowledgementPromise; + std::future acknowledgementFuture = acknowledgementPromise.get_future(); + valueSignal.getOnSubscribeComplete() += + [&acknowledgementPromise](MirroredSignalConfigPtr&, SubscriptionEventArgsPtr& args) + { + try + { + acknowledgementPromise.set_value(args.getStreamingConnectionString()); + } + catch(const std::exception& e) + { + FAIL() << e.what(); + } + }; + auto reader = PacketReader(valueSignal); + + // make value signal supported, it will trigger subscribe ack + testValueSignal.asPtr().setDescriptor(supportedValueDescriptor); + server->broadcastPacket(testValueSignal.getGlobalId(), DataDescriptorChangedEventPacket(supportedValueDescriptor, nullptr)); + + // wait for subscribe ack + ASSERT_EQ(acknowledgementFuture.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + // wait for new descriptors assigned + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // Check the updated mirrored signals + ASSERT_TRUE(valueSignal.getDescriptor().assigned()); + ASSERT_TRUE(domainSignal.getDescriptor().assigned()); + ASSERT_FALSE(domainSignal.getDomainSignal().assigned()); + ASSERT_EQ(valueSignal.getDomainSignal(), domainSignal); + ASSERT_TRUE(BaseObjectPtr::Equals(valueSignal.getDescriptor(), supportedValueDescriptor)); + ASSERT_TRUE(BaseObjectPtr::Equals(domainSignal.getDescriptor(), testDomainSignal.getDescriptor())); +} + +TEST_P(UnsupportedSignalsTestP, MakeDomainSignalSupported) +{ + SKIP_TEST_MAC_CI; + // Create server signals + auto testDomainSignal = streaming_test_helpers::createLinearTimeSignal(context); + auto testValueSignal = streaming_test_helpers::createExplicitValueSignal(context, "TestName", testDomainSignal); + auto signals = List(testValueSignal, testDomainSignal); + + const auto supportedDomainDescriptor = DataDescriptorBuilderCopy(testDomainSignal.getDescriptor()).build(); + // Set unsupported descriptor + testDomainSignal.asPtr().setDescriptor(unsupportedDescriptor); + + // Setup and start server which will publish created signals + auto server = std::make_shared(context); + server->onAccept([&](const daq::streaming_protocol::StreamWriterPtr& writer) { return signals; }); + server->start(STREAMING_PORT, CONTROL_PORT); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + clientDevice.asPtr().enableCoreEventTrigger(); + + ASSERT_EQ(clientDevice.getSignals().getCount(), 2u); + auto valueSignal = clientDevice.getSignals()[0].asPtr(); + auto domainSignal = clientDevice.getSignals()[1].asPtr(); + ASSERT_FALSE(valueSignal.getDescriptor().assigned()); + ASSERT_FALSE(domainSignal.getDescriptor().assigned()); + + // Wait for the signals temporarily subscribed by the client during initialization to be unsubscribed, + // to avoid interfering with the explicit subscription triggered by reader creation. + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // subscribe value signal + std::promise acknowledgementPromise; + std::future acknowledgementFuture = acknowledgementPromise.get_future(); + valueSignal.getOnSubscribeComplete() += + [&acknowledgementPromise](MirroredSignalConfigPtr&, SubscriptionEventArgsPtr& args) + { + try + { + acknowledgementPromise.set_value(args.getStreamingConnectionString()); + } + catch(const std::exception& e) + { + FAIL() << e.what(); + } + }; + auto reader = PacketReader(valueSignal); + + // make value signal supported, it will trigger subscribe ack + testDomainSignal.asPtr().setDescriptor(supportedDomainDescriptor); + server->broadcastPacket(testValueSignal.getGlobalId(), DataDescriptorChangedEventPacket(nullptr, supportedDomainDescriptor)); + + // wait for subscribe ack + ASSERT_EQ(acknowledgementFuture.wait_for(std::chrono::milliseconds(500)), std::future_status::ready); + + // wait for new descriptors assigned + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + + // Check the updated mirrored signals + ASSERT_TRUE(valueSignal.getDescriptor().assigned()); + ASSERT_TRUE(domainSignal.getDescriptor().assigned()); + ASSERT_FALSE(domainSignal.getDomainSignal().assigned()); + ASSERT_EQ(valueSignal.getDomainSignal(), domainSignal); + ASSERT_TRUE(BaseObjectPtr::Equals(domainSignal.getDescriptor(), supportedDomainDescriptor)); + ASSERT_TRUE(BaseObjectPtr::Equals(valueSignal.getDescriptor(), testValueSignal.getDescriptor())); +} + +INSTANTIATE_TEST_SUITE_P(UnsupportedSignalsTest, + UnsupportedSignalsTestP, + testing::Values(nullptr, DataDescriptorBuilder().setSampleType(daq::SampleType::Invalid).build())); + +TEST_F(WebsocketClientDeviceTest, DeviceWithMultipleSignals) +{ + // Create server side device + auto serverInstance = streaming_test_helpers::createServerInstance(); + + // Setup and start streaming server + auto server = WebsocketStreamingServer(serverInstance); + server.setStreamingPort(STREAMING_PORT); + server.setControlPort(CONTROL_PORT); + server.start(); + + // Create the client device + auto clientDevice = WebsocketClientDevice(NullContext(), nullptr, "device", HOST); + + // There should not be any difference if we get signals recursively or not, + // since client device doesn't know anything about hierarchy + size_t expectedSignalCount = 0; + for (const auto& signal : serverInstance.getSignals(search::Recursive(search::Visible()))) + expectedSignalCount += signal.getPublic(); + + ListPtr signals; + ASSERT_NO_THROW(signals = clientDevice.getSignals()); + ASSERT_EQ(signals.getCount(), expectedSignalCount); + ASSERT_NO_THROW(signals = clientDevice.getSignals(search::Recursive(search::Visible()))); + ASSERT_EQ(signals.getCount(), expectedSignalCount); +}