diff --git a/README.md b/README.md index 55d322ba..ef6f285f 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -MTConnect C++ Agent Version 2.3 +MTConnect C++ Agent Version 2.5 -------- [![Build MTConnect C++ Agent](https://github.com/mtconnect/cppagent/actions/workflows/build.yml/badge.svg)](https://github.com/mtconnect/cppagent/actions/workflows/build.yml) @@ -13,6 +13,10 @@ the devices and the location of the adapter. Pre-built binary releases for Windows are available from [Releases](https://github.com/mtconnect/cppagent/releases) for those who do not want to build the agent themselves. For *NIX users, you will need libxml2, cppunit, and cmake as well as build essentials. +Version 2.5.0 Added validation of observations in the stream + +Version 2.4.0 Added support for version 2.4 + Version 2.3.0 Support for all Version 2.3 standard changes and JSON ingress to MQTT adapter. Version 2.2.0 Support for all Version 2.2 standard changes and dynamic configuration from adapters. Upgrade to conan 2. @@ -639,11 +643,17 @@ Configuration Parameters * `SuppressIPAddress` - Suppress the Adapter IP Address and port when creating the Agent Device ids and names. This applies to all adapters. *Default*: false + +* `Validation` - Turns on validation of model components and observations + + *Default*: false * `WorkerThreads` - The number of operating system threads dedicated to the Agent *Default*: 1 + + #### Adapter General Configuration These can be overridden on a per-adapter basis diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index fbbbd70c..9571fc07 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -246,13 +246,11 @@ set(AGENT_SOURCES # src/sink/mqtt_sink HEADER_FILE_ONLY - "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.hpp" - "${SOURCE_DIR}/sink/mqtt_sink/mqtt2_service.hpp" + "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.hpp" #src/sink/mqtt_sink SOURCE_FILES_ONLY - "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" - "${SOURCE_DIR}/sink/mqtt_sink/mqtt2_service.cpp" + "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" # src/sink/rest_sink HEADER_FILE_ONLY @@ -267,6 +265,7 @@ set(AGENT_SOURCES "${SOURCE_DIR}/sink/rest_sink/session.hpp" "${SOURCE_DIR}/sink/rest_sink/session_impl.hpp" "${SOURCE_DIR}/sink/rest_sink/tls_dector.hpp" + "${SOURCE_DIR}/sink/rest_sink/websocket_session.hpp" # src/sink/rest_sink SOURCE_FILES_ONLY @@ -335,7 +334,6 @@ if(MSVC) # The modules including Beast required the /bigobj option in Windows set_property(SOURCE "${SOURCE_DIR}/sink/mqtt_sink/mqtt_service.cpp" - "${SOURCE_DIR}/sink/mqtt_sink/mqtt2_service.cpp" "${SOURCE_DIR}/sink/rest_sink/session_impl.cpp" "${SOURCE_DIR}/source/adapter/mqtt/mqtt_adapter.cpp" "${SOURCE_DIR}/source/adapter/agent_adapter/agent_adapter.cpp" diff --git a/conan/mqtt_cpp/conanfile.py b/conan/mqtt_cpp/conanfile.py index 456503ef..308a349c 100644 --- a/conan/mqtt_cpp/conanfile.py +++ b/conan/mqtt_cpp/conanfile.py @@ -10,7 +10,7 @@ class MqttcppConan(ConanFile): url = "https://github.com/redboltz/mqtt_cpp" description = "MQTT client/server for C++14 based on Boost.Asio" topics = ("mqtt") - requires = ["boost/1.82.0"] + requires = ["boost/1.84.0"] no_copy_source = True exports_sources = "include/*" diff --git a/conan/profiles/vs32 b/conan/profiles/vs32 index f78b6c43..fb406b01 100644 --- a/conan/profiles/vs32 +++ b/conan/profiles/vs32 @@ -8,3 +8,5 @@ compiler.runtime=static compiler.runtime_type=Release build_type=Release +[options] +winver=0x0600 diff --git a/conan/profiles/vs32debug b/conan/profiles/vs32debug index 44563790..54344e40 100644 --- a/conan/profiles/vs32debug +++ b/conan/profiles/vs32debug @@ -7,3 +7,6 @@ arch=x86 compiler.runtime=static compiler.runtime_type=Debug build_type=Debug + +[options] +winver=0x0600 diff --git a/conan/profiles/vs32shared b/conan/profiles/vs32shared index cac3622e..9bf550db 100644 --- a/conan/profiles/vs32shared +++ b/conan/profiles/vs32shared @@ -10,3 +10,4 @@ build_type=Release [options] shared=True +winver=0x0600 diff --git a/conanfile.py b/conanfile.py index 5dfe8587..02cd0438 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,7 +35,7 @@ class MTConnectAgentConan(ConanFile): "with_ruby": True, "development": False, "shared": False, - "winver": "0x600", + "winver": "0x0602", "with_docs": False, "cpack": False, "agent_prefix": None, @@ -118,7 +118,7 @@ def build_requirements(self): self.tool_requires_version("doxygen", [1, 9, 4]) def requirements(self): - self.requires("boost/1.82.0", headers=True, libs=True, transitive_headers=True, transitive_libs=True) + self.requires("boost/1.84.0", headers=True, libs=True, transitive_headers=True, transitive_libs=True) self.requires("libxml2/2.10.3", headers=True, libs=True, visible=True, transitive_headers=True, transitive_libs=True) self.requires("date/2.4.1", headers=True, libs=True, transitive_headers=True, transitive_libs=True) self.requires("nlohmann_json/3.9.1", headers=True, libs=False, transitive_headers=True, transitive_libs=False) @@ -139,6 +139,9 @@ def configure(self): if self.options.shared: self.options["boost/*"].shared = True self.package_type = "shared-library" + + if is_msvc(self): + self.options["boost/*"].extra_b2_flags = ("define=BOOST_USE_WINAPI_VERSION=" + str(self.options.winver)) # Make sure shared builds use shared boost if is_msvc(self) and self.options.shared: @@ -227,6 +230,7 @@ def package_info(self): winver=str(self.options.winver) self.cpp_info.defines.append("WINVER=" + winver) self.cpp_info.defines.append("_WIN32_WINNT=" + winver) + self.cpp_info.defines.append("BOOST_USE_WINAPI_VERSION=" + winver) def package(self): cmake = CMake(self) diff --git a/docker/ubuntu/Dockerfile b/docker/ubuntu/Dockerfile index 7221661d..7372a546 100644 --- a/docker/ubuntu/Dockerfile +++ b/docker/ubuntu/Dockerfile @@ -63,7 +63,7 @@ RUN apt-get update \ rake \ ruby \ && rm -rf /var/lib/apt/lists/* \ - && pip install conan -v 'conan==2.0.9' + && pip install conan # make an agent directory and cd into it WORKDIR /root/agent diff --git a/src/mtconnect/agent.cpp b/src/mtconnect/agent.cpp index 864d7f1a..958912a3 100644 --- a/src/mtconnect/agent.cpp +++ b/src/mtconnect/agent.cpp @@ -299,7 +299,7 @@ namespace mtconnect { { if (item.expired()) continue; - + auto di = item.lock(); if (di->hasInitialValue()) { @@ -307,7 +307,7 @@ namespace mtconnect { } } } - + std::lock_guard lock(m_circularBuffer); if (m_circularBuffer.addToBuffer(observation) != 0) { diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index bd134930..e9bf4013 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -58,7 +58,6 @@ #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/device_model/device.hpp" #include "mtconnect/printer/xml_printer.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt2_service.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" #include "mtconnect/sink/rest_sink/rest_service.hpp" #include "mtconnect/source/adapter/agent_adapter/agent_adapter.hpp" @@ -113,7 +112,6 @@ namespace mtconnect::configuration { bool success = false; sink::mqtt_sink::MqttService::registerFactory(m_sinkFactory); - sink::mqtt_sink::Mqtt2Service::registerFactory(m_sinkFactory); sink::rest_sink::RestService::registerFactory(m_sinkFactory); adapter::shdr::ShdrAdapter::registerFactory(m_sourceFactory); adapter::mqtt_adapter::MqttAdapter::registerFactory(m_sourceFactory); diff --git a/src/mtconnect/device_model/data_item/data_item.cpp b/src/mtconnect/device_model/data_item/data_item.cpp index 53f238b9..b0596fbd 100644 --- a/src/mtconnect/device_model/data_item/data_item.cpp +++ b/src/mtconnect/device_model/data_item/data_item.cpp @@ -201,7 +201,7 @@ namespace mtconnect { } } } - + if (const auto &init = maybeGet("InitialValue"); init) { m_initialValue = *init; diff --git a/src/mtconnect/device_model/data_item/data_item.hpp b/src/mtconnect/device_model/data_item/data_item.hpp index 7f62fdbd..8a7c6959 100644 --- a/src/mtconnect/device_model/data_item/data_item.hpp +++ b/src/mtconnect/device_model/data_item/data_item.hpp @@ -139,11 +139,11 @@ namespace mtconnect { /// @brief get the topic name leaf node for this data item /// @return the topic name const auto &getTopicName() const { return m_topicName; } - + /// @brief get the initial value if one is set /// @return optional initial value const auto &getInitialValue() const { return m_initialValue; } - + Category getCategory() const { return m_category; } Representation getRepresentation() const { return m_representation; } SpecialClass getSpecialClass() const { return m_specialClass; } diff --git a/src/mtconnect/mqtt/mqtt_client_impl.hpp b/src/mtconnect/mqtt/mqtt_client_impl.hpp index 56eb701c..8edeedc7 100644 --- a/src/mtconnect/mqtt/mqtt_client_impl.hpp +++ b/src/mtconnect/mqtt/mqtt_client_impl.hpp @@ -164,7 +164,6 @@ namespace mtconnect { m_connected = false; if (m_handler && m_handler->m_disconnected) m_handler->m_disconnected(shared_from_this()); - m_handler->m_disconnected(shared_from_this()); if (m_running) { reconnect(); @@ -419,7 +418,7 @@ namespace mtconnect { { return static_pointer_cast(shared_from_this()); } - + /// @brief Get the Mqtt TCP Client /// @return pointer to the Mqtt TCP Client auto &getClient() @@ -501,7 +500,7 @@ namespace mtconnect { { return static_pointer_cast(shared_from_this()); } - + /// @brief Get the Mqtt TLS WebSocket Client /// @return pointer to the Mqtt TLS WebSocket Client auto &getClient() @@ -540,7 +539,7 @@ namespace mtconnect { { return static_pointer_cast(shared_from_this()); } - + /// @brief Get the Mqtt TLS WebSocket Client /// @return pointer to the Mqtt TLS WebSocket Client auto &getClient() diff --git a/src/mtconnect/observation/change_observer.cpp b/src/mtconnect/observation/change_observer.cpp index 88b5c5da..f489b287 100644 --- a/src/mtconnect/observation/change_observer.cpp +++ b/src/mtconnect/observation/change_observer.cpp @@ -29,14 +29,25 @@ using namespace std; namespace mtconnect::observation { ChangeObserver::~ChangeObserver() { + std::lock_guard scopedLock(m_mutex); + clear(); + } + + void ChangeObserver::clear() + { + std::unique_lock lock(m_mutex); + m_timer.cancel(); + m_handler.clear(); for (const auto signaler : m_signalers) signaler->removeObserver(this); + m_signalers.clear(); } void ChangeObserver::addSignaler(ChangeSignaler *sig) { m_signalers.emplace_back(sig); } bool ChangeObserver::removeSignaler(ChangeSignaler *sig) { + std::lock_guard scopedLock(m_mutex); auto newEndPos = std::remove(m_signalers.begin(), m_signalers.end(), sig); if (newEndPos == m_signalers.end()) return false; @@ -47,7 +58,8 @@ namespace mtconnect::observation { void ChangeObserver::handler(boost::system::error_code ec) { - boost::asio::post(m_strand, boost::bind(m_handler, ec)); + if (m_handler) + boost::asio::post(m_strand, boost::bind(m_handler, ec)); } // Signaler Management @@ -99,7 +111,7 @@ namespace mtconnect::observation { buffer::CircularBuffer &buffer, FilterSet &&filter, std::chrono::milliseconds interval, std::chrono::milliseconds heartbeat) - : m_interval(interval), + : AsyncResponse(interval), m_heartbeat(heartbeat), m_last(std::chrono::system_clock::now()), m_filter(std::move(filter)), @@ -145,9 +157,12 @@ namespace mtconnect::observation { void AsyncObserver::handlerCompleted() { + NAMED_SCOPE("AsyncObserver::handlerCompleted"); + m_last = std::chrono::system_clock::now(); if (m_endOfBuffer) { + LOG(trace) << "End of buffer"; using std::placeholders::_1; m_observer.waitForSignal(m_heartbeat); } @@ -159,6 +174,7 @@ namespace mtconnect::observation { void AsyncObserver::handleSignal(boost::system::error_code ec) { + NAMED_SCOPE("AsyncObserver::handleSignal"); using namespace buffer; using std::placeholders::_1; diff --git a/src/mtconnect/observation/change_observer.hpp b/src/mtconnect/observation/change_observer.hpp index 3f4e5c2e..c274e848 100644 --- a/src/mtconnect/observation/change_observer.hpp +++ b/src/mtconnect/observation/change_observer.hpp @@ -143,6 +143,9 @@ namespace mtconnect::observation { auto try_lock() { return m_mutex.try_lock(); } ///@} + /// @brief clear the observer information. + void clear(); + private: boost::asio::io_context::strand &m_strand; mutable std::recursive_mutex m_mutex; @@ -185,6 +188,32 @@ namespace mtconnect::observation { std::list m_observers; }; + /// @brief Abstract class for things asynchronouos timers + class AGENT_LIB_API AsyncResponse : public std::enable_shared_from_this + { + public: + AsyncResponse(std::chrono::milliseconds interval) : m_interval(interval) {} + + virtual bool cancel() = 0; + + /// @brief method to determine if the sink is running + virtual bool isRunning() = 0; + + /// @brief get the request id for webservices + const auto &getRequestId() const { return m_requestId; } + + /// @brief sets the optonal request id for webservices. + void setRequestId(const std::optional &id) { m_requestId = id; } + + /// @brief Get the interval + const auto &getInterval() const { return m_interval; } + + protected: + std::chrono::milliseconds m_interval { + 0}; //! the minimum amout of time to wait before calling the handler + std::optional m_requestId; //! request id + }; + /// @brief Asyncronous change context for waiting for changes /// /// This class must be subclassed and provide a fail and isRunning method. @@ -196,7 +225,7 @@ namespace mtconnect::observation { /// /// The handler and sequence numbers are handled inside the circular buffer lock to prevent race /// conditions with incoming data. - class AGENT_LIB_API AsyncObserver : public std::enable_shared_from_this + class AGENT_LIB_API AsyncObserver : public AsyncResponse { public: /// @Brief callback when observations are ready @@ -217,7 +246,7 @@ namespace mtconnect::observation { virtual ~AsyncObserver() = default; /// @brief Get a shared pointed - auto getptr() const { return const_cast(this)->shared_from_this(); } + auto getptr() { return std::dynamic_pointer_cast(shared_from_this()); } /// @brief sets up the `ChangeObserver` using the filter and initializes the references to the /// buffer @@ -235,8 +264,12 @@ namespace mtconnect::observation { /// @brief abstract call to failure handler virtual void fail(boost::beast::http::status status, const std::string &message) = 0; - /// @brief method to determine if the sink is running - virtual bool isRunning() = 0; + /// @brief Stop all timers and release resources. + bool cancel() override + { + m_observer.clear(); + return true; + } /// @brief handler callback when an action needs to be taken /// @@ -250,9 +283,7 @@ namespace mtconnect::observation { auto getSequence() const { return m_sequence; } auto isEndOfBuffer() const { return m_endOfBuffer; } const auto &getFilter() const { return m_filter; } - ///@} - /// mutable bool m_endOfBuffer {false}; //! Public indicator that we are at the end of the buffer @@ -266,8 +297,6 @@ namespace mtconnect::observation { protected: SequenceNumber_t m_sequence {0}; //! the current sequence number - std::chrono::milliseconds m_interval { - 0}; //! the minimum amout of time to wait before calling the handler std::chrono::milliseconds m_heartbeat { 0}; //! the maximum amount of time to wait before sending a heartbeat std::chrono::system_clock::time_point m_last; //! the last time the handler completed diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index 28720379..3aa2ea39 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -60,7 +60,8 @@ namespace mtconnect::printer { template inline void header(AutoJsonObject &obj, const string &version, const string &hostname, const uint64_t instanceId, const unsigned int bufferSize, - const string &schemaVersion, const string modelChangeTime, bool validation) + const string &schemaVersion, const string modelChangeTime, bool validation, + const std::optional &requestId) { obj.AddPairs("version", version, "creationTime", getCurrentTime(GMT), "testIndicator", false, "instanceId", instanceId, "sender", hostname, "schemaVersion", schemaVersion); @@ -71,6 +72,8 @@ namespace mtconnect::printer { obj.AddPairs("bufferSize", bufferSize); if (validation) obj.AddPairs("validation", true); + if (requestId) + obj.AddPairs("requestId", *requestId); } template @@ -78,10 +81,11 @@ namespace mtconnect::printer { const string &hostname, const uint64_t instanceId, const unsigned int bufferSize, const unsigned int assetBufferSize, const unsigned int assetCount, const string &schemaVersion, - const string modelChangeTime, const bool validation) + const string modelChangeTime, const bool validation, + const std::optional &requestId) { header(obj, version, hostname, instanceId, bufferSize, schemaVersion, modelChangeTime, - validation); + validation, requestId); obj.AddPairs("assetBufferSize", assetBufferSize, "assetCount", assetCount); } @@ -90,10 +94,11 @@ namespace mtconnect::printer { const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSequence, const uint64_t firstSequence, const uint64_t lastSequence, const string &schemaVersion, - const string modelChangeTime, const bool validation) + const string modelChangeTime, const bool validation, + const std::optional &requestId) { header(obj, version, hostname, instanceId, bufferSize, schemaVersion, modelChangeTime, - validation); + validation, requestId); obj.AddPairs("nextSequence", nextSequence, "lastSequence", lastSequence, "firstSequence", firstSequence); } @@ -113,7 +118,8 @@ namespace mtconnect::printer { std::string JsonPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty) const + bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); @@ -127,7 +133,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); header(obj, m_version, m_senderName, instanceId, bufferSize, *m_schemaVersion, - m_modelChangeTime, m_validation); + m_modelChangeTime, m_validation, requestId); } { if (m_jsonVersion > 1) @@ -170,7 +176,8 @@ namespace mtconnect::printer { const unsigned int assetCount, const std::list &devices, const std::map *count, - bool includeHidden, bool pretty) const + bool includeHidden, bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); @@ -184,7 +191,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); probeAssetHeader(obj, m_version, m_senderName, instanceId, bufferSize, assetBufferSize, - assetCount, *m_schemaVersion, m_modelChangeTime, m_validation); + assetCount, *m_schemaVersion, m_modelChangeTime, m_validation, requestId); } { obj.Key("Devices"); @@ -197,7 +204,8 @@ namespace mtconnect::printer { std::string JsonPrinter::printAssets(const uint64_t instanceId, const unsigned int bufferSize, const unsigned int assetCount, const asset::AssetList &asset, - bool pretty) const + bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); @@ -211,7 +219,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); probeAssetHeader(obj, m_version, m_senderName, instanceId, 0, bufferSize, assetCount, - *m_schemaVersion, m_modelChangeTime, m_validation); + *m_schemaVersion, m_modelChangeTime, m_validation, requestId); } { obj.Key("Assets"); @@ -397,7 +405,8 @@ namespace mtconnect::printer { std::string JsonPrinter::printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, ObservationList &observations, - bool pretty) const + bool pretty, + const std::optional requestId) const { defaultSchemaVersion(); @@ -409,7 +418,7 @@ namespace mtconnect::printer { { AutoJsonObject obj(writer, "Header"); streamHeader(obj, m_version, m_senderName, instanceId, bufferSize, nextSeq, firstSeq, - lastSeq, *m_schemaVersion, m_modelChangeTime, m_validation); + lastSeq, *m_schemaVersion, m_modelChangeTime, m_validation, requestId); } { diff --git a/src/mtconnect/printer/json_printer.hpp b/src/mtconnect/printer/json_printer.hpp index fd392133..119f77b8 100644 --- a/src/mtconnect/printer/json_printer.hpp +++ b/src/mtconnect/printer/json_printer.hpp @@ -30,23 +30,27 @@ namespace mtconnect::printer { JsonPrinter(uint32_t jsonVersion, bool pretty = false, bool validation = false); ~JsonPrinter() override = default; - std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false) const override; - - std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const unsigned int assetBufferSize, - const unsigned int assetCount, const std::list &devices, - const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false) const override; - - std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const uint64_t firstSeq, const uint64_t lastSeq, - observation::ObservationList &results, - bool pretty = false) const override; - std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, const asset::AssetList &asset, - bool pretty = false) const override; + std::string printErrors( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const ProtoErrorList &list, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printProbe( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const unsigned int assetBufferSize, const unsigned int assetCount, + const std::list &devices, const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printSample( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false, + const std::optional requestId = std::nullopt) const override; + std::string printAssets( + const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, + const asset::AssetList &asset, bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string mimeType() const override { return "application/mtconnect+json"; } uint32_t getJsonVersion() const { return m_jsonVersion; } diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index 52943c17..d6526a98 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -61,9 +61,10 @@ namespace mtconnect { /// @param[in] errorCode an error code /// @param[in] errorText the error text /// @return the error document - virtual std::string printError(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const std::string &errorCode, - const std::string &errorText, bool pretty = false) const + virtual std::string printError( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const std::string &errorCode, const std::string &errorText, bool pretty = false, + const std::optional requestId = std::nullopt) const { return printErrors(instanceId, bufferSize, nextSeq, {{errorCode, errorText}}); } @@ -73,9 +74,10 @@ namespace mtconnect { /// @param[in] nextSeq the next sequence /// @param[in] list the list of errors /// @return the MTConnect Error document - virtual std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false) const = 0; + virtual std::string printErrors( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const ProtoErrorList &list, bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Devices document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -85,12 +87,12 @@ namespace mtconnect { /// @param[in] devices a list of devices /// @param[in] count optional asset count and type association /// @return the MTConnect Devices document - virtual std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const unsigned int assetBufferSize, - const unsigned int assetCount, - const std::list &devices, - const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false) const = 0; + virtual std::string printProbe( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const unsigned int assetBufferSize, const unsigned int assetCount, + const std::list &devices, const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief Print a MTConnect Streams document /// @param[in] instanceId the instance id /// @param[in] bufferSize the buffer size @@ -99,19 +101,20 @@ namespace mtconnect { /// @param[in] lastSeq the last sequnce /// @param[in] results a list of observations /// @return the MTConnect Streams document - virtual std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, observation::ObservationList &results, - bool pretty = false) const = 0; + virtual std::string printSample( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false, const std::optional requestId = std::nullopt) const = 0; /// @brief Generate an MTConnect Assets document /// @param[in] anInstanceId the instance id /// @param[in] bufferSize the buffer size /// @param[in] assetCount the asset count /// @param[in] asset the list of assets /// @return the MTConnect Assets document - virtual std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, asset::AssetList const &asset, - bool pretty = false) const = 0; + virtual std::string printAssets( + const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, + asset::AssetList const &asset, bool pretty = false, + const std::optional requestId = std::nullopt) const = 0; /// @brief get the mime type for the documents /// @return the mime type virtual std::string mimeType() const = 0; diff --git a/src/mtconnect/printer/xml_printer.cpp b/src/mtconnect/printer/xml_printer.cpp index 56e05947..8a6db2f1 100644 --- a/src/mtconnect/printer/xml_printer.cpp +++ b/src/mtconnect/printer/xml_printer.cpp @@ -344,7 +344,7 @@ namespace mtconnect::printer { std::string XmlPrinter::printErrors(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty) const + bool pretty, const std::optional requestId) const { string ret; @@ -352,7 +352,8 @@ namespace mtconnect::printer { { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eERROR, instanceId, bufferSize, 0, 0, nextSeq, nextSeq - 1); + initXmlDoc(writer, eERROR, instanceId, bufferSize, 0, 0, nextSeq, 0, nextSeq - 1, nullptr, + requestId); { AutoElement e1(writer, "Errors"); @@ -382,7 +383,7 @@ namespace mtconnect::printer { const uint64_t nextSeq, const unsigned int assetBufferSize, const unsigned int assetCount, const list &deviceList, const std::map *count, bool includeHidden, - bool pretty) const + bool pretty, const std::optional requestId) const { string ret; @@ -391,7 +392,7 @@ namespace mtconnect::printer { XmlWriter writer(m_pretty || pretty); initXmlDoc(writer, eDEVICES, instanceId, bufferSize, assetBufferSize, assetCount, nextSeq, 0, - nextSeq - 1, count); + nextSeq - 1, count, requestId); { AutoElement devices(writer, "Devices"); @@ -418,8 +419,8 @@ namespace mtconnect::printer { string XmlPrinter::printSample(const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, ObservationList &observations, - bool pretty) const + const uint64_t lastSeq, ObservationList &observations, bool pretty, + const std::optional requestId) const { string ret; @@ -427,7 +428,8 @@ namespace mtconnect::printer { { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eSTREAMS, instanceId, bufferSize, 0, 0, nextSeq, firstSeq, lastSeq); + initXmlDoc(writer, eSTREAMS, instanceId, bufferSize, 0, 0, nextSeq, firstSeq, lastSeq, + nullptr, requestId); AutoElement streams(writer, "Streams"); @@ -498,14 +500,15 @@ namespace mtconnect::printer { } string XmlPrinter::printAssets(const uint64_t instanceId, const unsigned int bufferSize, - const unsigned int assetCount, const AssetList &asset, - bool pretty) const + const unsigned int assetCount, const AssetList &asset, bool pretty, + const std::optional requestId) const { string ret; try { XmlWriter writer(m_pretty || pretty); - initXmlDoc(writer, eASSETS, instanceId, 0u, bufferSize, assetCount, 0ull); + initXmlDoc(writer, eASSETS, instanceId, 0u, bufferSize, assetCount, 0ull, 0, 0, nullptr, + requestId); { AutoElement ele(writer, "Assets"); @@ -541,7 +544,8 @@ namespace mtconnect::printer { const uint64_t instanceId, const unsigned int bufferSize, const unsigned int assetBufferSize, const unsigned int assetCount, const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, const map *count) const + const uint64_t lastSeq, const map *count, + const std::optional requestId) const { THROW_IF_XML2_ERROR(xmlTextWriterStartDocument(writer, nullptr, "UTF-8", nullptr)); @@ -648,6 +652,9 @@ namespace mtconnect::printer { AGENT_VERSION_BUILD); addAttribute(writer, "version", version); + if (requestId) + addAttribute(writer, "requestId", *requestId); + int major, minor; char c; stringstream v(*m_schemaVersion); diff --git a/src/mtconnect/printer/xml_printer.hpp b/src/mtconnect/printer/xml_printer.hpp index 781a297c..d5eba57d 100644 --- a/src/mtconnect/printer/xml_printer.hpp +++ b/src/mtconnect/printer/xml_printer.hpp @@ -43,23 +43,27 @@ namespace mtconnect { XmlPrinter(bool pretty = false, bool validation = false); ~XmlPrinter() override = default; - std::string printErrors(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const ProtoErrorList &list, - bool pretty = false) const override; - - std::string printProbe(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const unsigned int assetBufferSize, - const unsigned int assetCount, const std::list &devices, - const std::map *count = nullptr, - bool includeHidden = false, bool pretty = false) const override; - - std::string printSample(const uint64_t instanceId, const unsigned int bufferSize, - const uint64_t nextSeq, const uint64_t firstSeq, - const uint64_t lastSeq, observation::ObservationList &results, - bool pretty = false) const override; - std::string printAssets(const uint64_t anInstanceId, const unsigned int bufferSize, - const unsigned int assetCount, const asset::AssetList &asset, - bool pretty = false) const override; + std::string printErrors( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const ProtoErrorList &list, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printProbe( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const unsigned int assetBufferSize, const unsigned int assetCount, + const std::list &devices, const std::map *count = nullptr, + bool includeHidden = false, bool pretty = false, + const std::optional requestId = std::nullopt) const override; + + std::string printSample( + const uint64_t instanceId, const unsigned int bufferSize, const uint64_t nextSeq, + const uint64_t firstSeq, const uint64_t lastSeq, observation::ObservationList &results, + bool pretty = false, + const std::optional requestId = std::nullopt) const override; + std::string printAssets( + const uint64_t anInstanceId, const unsigned int bufferSize, const unsigned int assetCount, + const asset::AssetList &asset, bool pretty = false, + const std::optional requestId = std::nullopt) const override; std::string mimeType() const override { return "text/xml"; } /// @brief Add a Devices XML device namespace @@ -167,7 +171,8 @@ namespace mtconnect { const unsigned int bufferSize, const unsigned int assetBufferSize, const unsigned int assetCount, const uint64_t nextSeq, const uint64_t firstSeq = 0, const uint64_t lastSeq = 0, - const std::map *counts = nullptr) const; + const std::map *counts = nullptr, + const std::optional requestId = std::nullopt) const; // Helper to print individual components and details void printProbeHelper(xmlTextWriterPtr writer, device_model::ComponentPtr component, diff --git a/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp b/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp deleted file mode 100644 index 0a859fa8..00000000 --- a/src/mtconnect/sink/mqtt_sink/mqtt2_service.cpp +++ /dev/null @@ -1,352 +0,0 @@ -// -// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// 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. -// - -#include "mqtt2_service.hpp" - -#include - -#include "mtconnect/configuration/config_options.hpp" -#include "mtconnect/entity/entity.hpp" -#include "mtconnect/entity/factory.hpp" -#include "mtconnect/entity/json_parser.hpp" -#include "mtconnect/mqtt/mqtt_client_impl.hpp" -#include "mtconnect/printer/json_printer.hpp" - -using ptree = boost::property_tree::ptree; -using json = nlohmann::json; - -using namespace std; -using namespace mtconnect; -using namespace mtconnect::asset; - -namespace asio = boost::asio; -namespace config = ::mtconnect::configuration; - -namespace mtconnect { - namespace sink { - namespace mqtt_sink { - // get obeservation in - // create a json printer - // call print - - Mqtt2Service::Mqtt2Service(boost::asio::io_context &context, sink::SinkContractPtr &&contract, - const ConfigOptions &options, const ptree &config) - : Sink("Mqtt2Service", std::move(contract)), - m_context(context), - m_strand(context), - m_options(options), - m_currentTimer(context) - { - // Unique id number for agent instance - m_instanceId = getCurrentTimeInSec(); - - auto jsonPrinter = dynamic_cast(m_sinkContract->getPrinter("json")); - - m_jsonPrinter = make_unique(jsonPrinter->getJsonVersion()); - - m_printer = std::make_unique(jsonPrinter->getJsonVersion()); - - GetOptions(config, m_options, options); - AddOptions(config, m_options, - {{configuration::ProbeTopic, string()}, - {configuration::MqttCaCert, string()}, - {configuration::MqttPrivateKey, string()}, - {configuration::MqttCert, string()}, - {configuration::MqttClientId, string()}, - {configuration::MqttUserName, string()}, - {configuration::MqttPassword, string()}}); - AddDefaultedOptions( - config, m_options, - {{configuration::MqttHost, "127.0.0.1"s}, - {configuration::DeviceTopic, "MTConnect/Probe/[device]"s}, - {configuration::AssetTopic, "MTConnect/Asset/[device]"s}, - {configuration::MqttLastWillTopic, "MTConnect/Probe/[device]/Availability"s}, - {configuration::CurrentTopic, "MTConnect/Current/[device]"s}, - {configuration::SampleTopic, "MTConnect/Sample/[device]"s}, - {configuration::MqttCurrentInterval, 10000ms}, - {configuration::MqttSampleInterval, 500ms}, - {configuration::MqttSampleCount, 1000}, - {configuration::MqttPort, 1883}, - {configuration::MqttTls, false}}); - - int maxTopicDepth {GetOption(options, configuration::MqttMaxTopicDepth).value_or(7)}; - - m_deviceTopic = GetOption(m_options, configuration::ProbeTopic) - .value_or(get(m_options[configuration::DeviceTopic])); - m_assetTopic = getTopic(configuration::AssetTopic, maxTopicDepth); - m_currentTopic = getTopic(configuration::CurrentTopic, maxTopicDepth); - m_sampleTopic = getTopic(configuration::SampleTopic, maxTopicDepth); - - m_currentInterval = *GetOption(m_options, configuration::MqttCurrentInterval); - m_sampleInterval = *GetOption(m_options, configuration::MqttSampleInterval); - - m_sampleCount = *GetOption(m_options, configuration::MqttSampleCount); - } - - void Mqtt2Service::start() - { - if (!m_client) - { - auto clientHandler = make_unique(); - clientHandler->m_connected = [this](shared_ptr client) { - // Publish latest devices, assets, and observations - auto &circ = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(circ); - client->connectComplete(); - - client->publish(m_lastWillTopic, "AVAILABLE"); - pubishInitialContent(); - }; - - auto agentDevice = m_sinkContract->getDeviceByName("Agent"); - auto lwtTopic = get(m_options[configuration::MqttLastWillTopic]); - m_lastWillTopic = formatTopic(lwtTopic, agentDevice, "Agent"); - - if (IsOptionSet(m_options, configuration::MqttTls)) - { - m_client = make_shared(m_context, m_options, std::move(clientHandler), - m_lastWillTopic, "UNAVAILABLE"s); - } - else - { - m_client = make_shared(m_context, m_options, std::move(clientHandler), - m_lastWillTopic, "UNAVAILABLE"s); - } - } - m_client->start(); - } - - void Mqtt2Service::stop() - { - // stop client side - if (m_client) - m_client->stop(); - - m_currentTimer.cancel(); - } - - struct AsyncSample : public observation::AsyncObserver - { - AsyncSample(boost::asio::io_context::strand &strand, - mtconnect::buffer::CircularBuffer &buffer, FilterSet &&filter, - std::chrono::milliseconds interval, std::chrono::milliseconds heartbeat, - std::shared_ptr client, DevicePtr device) - : observation::AsyncObserver(strand, buffer, std::move(filter), interval, heartbeat), - m_device(device), - m_client(client) - {} - - void fail(boost::beast::http::status status, const std::string &message) override - { - LOG(error) << "MQTT Sample Failed: " << message; - } - - bool isRunning() override - { - if (m_sink.expired()) - return false; - - auto client = m_client.lock(); - return client && client->isRunning() && client->isConnected(); - } - - DevicePtr m_device; - std::weak_ptr m_client; - std::weak_ptr - m_sink; //! weak shared pointer to the sink. handles shutdown timer race - }; - - void Mqtt2Service::pubishInitialContent() - { - using std::placeholders::_1; - for (auto &dev : m_sinkContract->getDevices()) - { - publish(dev); - - AssetList list; - m_sinkContract->getAssetStorage()->getAssets(list, 100000, true, *(dev->getUuid())); - for (auto &asset : list) - { - publish(asset); - } - } - - auto seq = m_sinkContract->getCircularBuffer().getSequence(); - for (auto &dev : m_sinkContract->getDevices()) - { - FilterSet filterSet = filterForDevice(dev); - auto sampler = - make_shared(m_strand, m_sinkContract->getCircularBuffer(), - std::move(filterSet), m_sampleInterval, 600s, m_client, dev); - sampler->m_sink = getptr(); - sampler->m_handler = boost::bind(&Mqtt2Service::publishSample, this, _1); - sampler->observe(seq, [this](const std::string &id) { - return m_sinkContract->getDataItemById(id).get(); - }); - sampler->handlerCompleted(); - } - - publishCurrent(boost::system::error_code {}); - } - - /// @brief publish sample when observations arrive. - SequenceNumber_t Mqtt2Service::publishSample( - std::shared_ptr observer) - { - auto sampler = std::dynamic_pointer_cast(observer); - auto topic = formatTopic(m_sampleTopic, sampler->m_device); - LOG(debug) << "Publishing sample for: " << topic; - - std::unique_ptr observations; - SequenceNumber_t end {0}; - std::string doc; - SequenceNumber_t firstSeq, lastSeq; - - { - auto &buffer = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(buffer); - - lastSeq = buffer.getSequence() - 1; - observations = - buffer.getObservations(m_sampleCount, sampler->getFilter(), sampler->getSequence(), - nullopt, end, firstSeq, observer->m_endOfBuffer); - } - - doc = m_printer->printSample(m_instanceId, - m_sinkContract->getCircularBuffer().getBufferSize(), end, - firstSeq, lastSeq, *observations, false); - - m_client->asyncPublish(topic, doc, [sampler, topic](std::error_code ec) { - if (!ec) - { - sampler->handlerCompleted(); - } - else - { - LOG(warning) << "Async publish failed for " << topic << ": " << ec.message(); - } - }); - - return end; - } - - void Mqtt2Service::publishCurrent(boost::system::error_code ec) - { - if (ec) - { - LOG(warning) << "Mqtt2Service::publishCurrent: " << ec.message(); - return; - } - - if (!m_client->isRunning() || !m_client->isConnected()) - { - LOG(warning) << "Mqtt2Service::publishCurrent: client stopped"; - return; - } - - for (auto &device : m_sinkContract->getDevices()) - { - auto topic = formatTopic(m_currentTopic, device); - LOG(debug) << "Publishing current for: " << topic; - - ObservationList observations; - SequenceNumber_t firstSeq, seq; - auto filterSet = filterForDevice(device); - - { - auto &buffer = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(buffer); - - firstSeq = buffer.getFirstSequence(); - seq = buffer.getSequence(); - m_sinkContract->getCircularBuffer().getLatest().getObservations(observations, - filterSet); - } - - auto doc = m_printer->printSample(m_instanceId, - m_sinkContract->getCircularBuffer().getBufferSize(), - seq, firstSeq, seq - 1, observations); - - m_client->publish(topic, doc); - } - - using std::placeholders::_1; - m_currentTimer.expires_after(m_currentInterval); - m_currentTimer.async_wait(boost::asio::bind_executor( - m_strand, boost::bind(&Mqtt2Service::publishCurrent, this, _1))); - } - - bool Mqtt2Service::publish(observation::ObservationPtr &observation) - { - // Since we are doing periodic publishing, there is nothing to do here. - return true; - } - - bool Mqtt2Service::publish(device_model::DevicePtr device) - { - m_filters.clear(); - - auto topic = formatTopic(m_deviceTopic, device); - auto doc = m_jsonPrinter->print(device); - - stringstream buffer; - buffer << doc; - - if (m_client) - m_client->publish(topic, buffer.str()); - - return true; - } - - bool Mqtt2Service::publish(asset::AssetPtr asset) - { - auto uuid = asset->getDeviceUuid(); - DevicePtr dev; - if (uuid) - dev = m_sinkContract->findDeviceByUUIDorName(*uuid); - auto topic = formatTopic(m_assetTopic, dev); - if (topic.back() != '/') - topic.append("/"); - topic.append(asset->getAssetId()); - - LOG(debug) << "Publishing Asset to topic: " << topic; - - auto doc = m_jsonPrinter->print(asset); - - stringstream buffer; - buffer << doc; - - if (m_client) - m_client->publish(topic, buffer.str()); - - return true; - } - - // Register the service with the sink factory - void Mqtt2Service::registerFactory(SinkFactory &factory) - { - factory.registerFactory( - "Mqtt2Service", - [](const std::string &name, boost::asio::io_context &io, SinkContractPtr &&contract, - const ConfigOptions &options, const boost::property_tree::ptree &block) -> SinkPtr { - auto sink = std::make_shared(io, std::move(contract), options, block); - return sink; - }); - } - } // namespace mqtt_sink - } // namespace sink -} // namespace mtconnect diff --git a/src/mtconnect/sink/mqtt_sink/mqtt2_service.hpp b/src/mtconnect/sink/mqtt_sink/mqtt2_service.hpp deleted file mode 100644 index 0e1ddbab..00000000 --- a/src/mtconnect/sink/mqtt_sink/mqtt2_service.hpp +++ /dev/null @@ -1,205 +0,0 @@ -// -// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// 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 "boost/asio/io_context.hpp" -#include - -#include - -#include "mtconnect/buffer/checkpoint.hpp" -#include "mtconnect/config.hpp" -#include "mtconnect/configuration/agent_config.hpp" -#include "mtconnect/entity/json_printer.hpp" -#include "mtconnect/mqtt/mqtt_client.hpp" -#include "mtconnect/observation/observation.hpp" -#include "mtconnect/printer//json_printer.hpp" -#include "mtconnect/printer/printer.hpp" -#include "mtconnect/printer/xml_printer_helper.hpp" -#include "mtconnect/sink/sink.hpp" -#include "mtconnect/utilities.hpp" - -using namespace std; -using namespace mtconnect; -using namespace mtconnect::entity; -using namespace mtconnect::mqtt_client; - -using json = nlohmann::json; - -namespace mtconnect { - class XmlPrinter; - - namespace sink { - - /// @brief MTConnect Mqtt implemention namespace - - namespace mqtt_sink { - - struct AsyncSample; - - class AGENT_LIB_API Mqtt2Service : public sink::Sink - { - // dynamic loading of sink - - public: - /// @brief Create a Mqtt Service sink - /// @param context the boost asio io_context - /// @param contract the Sink Contract from the agent - /// @param options configuration options - /// @param config additional configuration options if specified directly as a sink - Mqtt2Service(boost::asio::io_context &context, sink::SinkContractPtr &&contract, - const ConfigOptions &options, const boost::property_tree::ptree &config); - - ~Mqtt2Service() = default; - - // Sink Methods - /// @brief Start the Mqtt service - void start() override; - - /// @brief Shutdown the Mqtt service - void stop() override; - - /// @brief Receive an observation - /// - /// This does nothing since we are periodically publishing current and samples - /// - /// @param observation shared pointer to the observation - /// @return `true` if the publishing was successful - bool publish(observation::ObservationPtr &observation) override; - - /// @brief Receive an asset - /// @param asset shared point to the asset - /// @return `true` if successful - bool publish(asset::AssetPtr asset) override; - - /// @brief Receive a device - /// @param device shared pointer to the device - /// @return `true` if successful - bool publish(device_model::DevicePtr device) override; - - /// @brief Publsh all devices, assets, and begin async timer-based publishing - void pubishInitialContent(); - - /// @brief Publish a current using `CurrentInterval` option. - void publishCurrent(boost::system::error_code ec); - - /// @brief publish sample when observations arrive. - SequenceNumber_t publishSample(std::shared_ptr sampler); - - /// @brief Register the Sink factory to create this sink - /// @param factory - static void registerFactory(SinkFactory &factory); - - /// @brief gets a Mqtt Client - /// @return MqttClient - std::shared_ptr getClient() { return m_client; } - - /// @brief Mqtt Client is Connected or not - /// @return `true` when the client was connected - bool isConnected() { return m_client && m_client->isConnected(); } - - protected: - const FilterSet &filterForDevice(const DevicePtr &device) - { - auto filter = m_filters.find(*(device->getUuid())); - if (filter == m_filters.end()) - { - auto pos = m_filters.emplace(*(device->getUuid()), FilterSet()); - filter = pos.first; - auto &set = filter->second; - for (const auto &wdi : device->getDeviceDataItems()) - { - const auto di = wdi.lock(); - if (di) - set.insert(di->getId()); - } - } - return filter->second; - } - - std::string formatTopic(const std::string &topic, const DevicePtr device, - const std::string defaultUuid = "Unknown") - { - string uuid; - string formatted {topic}; - if (!device) - uuid = defaultUuid; - else - { - uuid = *(device->getUuid()); - if (std::dynamic_pointer_cast(device)) - { - uuid.insert(0, "Agent_"); - } - } - - if (formatted.find("[device]") == std::string::npos) - { - if (formatted.back() != '/') - formatted.append("/"); - formatted.append(uuid); - } - else - { - boost::replace_all(formatted, "[device]", uuid); - } - return formatted; - } - - std::string getTopic(const std::string &option, int maxTopicDepth) - { - auto topic {get(m_options[option])}; - auto depth = std::count(topic.begin(), topic.end(), '/'); - - if (depth > maxTopicDepth) - LOG(warning) << "Mqtt Option " << option - << " exceeds maximum number of levels: " << maxTopicDepth; - - return topic; - } - - protected: - std::string m_deviceTopic; //! Device topic prefix - std::string m_assetTopic; //! Asset topic prefix - std::string m_currentTopic; //! Current topic prefix - std::string m_sampleTopic; //! Sample topic prefix - std::string m_lastWillTopic; //! Topic to publish the last will when disconnected - - std::chrono::milliseconds m_currentInterval; //! Interval in ms to update current - std::chrono::milliseconds m_sampleInterval; //! min interval in ms to update sample - - uint64_t m_instanceId; - - boost::asio::io_context &m_context; - boost::asio::io_context::strand m_strand; - - ConfigOptions m_options; - - std::unique_ptr m_jsonPrinter; - std::unique_ptr m_printer; - - std::shared_ptr m_client; - boost::asio::steady_timer m_currentTimer; - int m_sampleCount; //! Timer for current requests - - std::map m_filters; - std::map> m_samplers; - }; - } // namespace mqtt_sink - } // namespace sink -} // namespace mtconnect diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp b/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp index b28b72d0..7c17edbf 100644 --- a/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp +++ b/src/mtconnect/sink/mqtt_sink/mqtt_service.cpp @@ -1,5 +1,5 @@ // -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,8 @@ #include "mqtt_service.hpp" +#include + #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/entity/entity.hpp" #include "mtconnect/entity/factory.hpp" @@ -25,8 +27,10 @@ #include "mtconnect/printer/json_printer.hpp" using ptree = boost::property_tree::ptree; +using json = nlohmann::json; using namespace std; +using namespace mtconnect; using namespace mtconnect::asset; namespace asio = boost::asio; @@ -41,119 +45,264 @@ namespace mtconnect { MqttService::MqttService(boost::asio::io_context &context, sink::SinkContractPtr &&contract, const ConfigOptions &options, const ptree &config) - : Sink("MqttService", std::move(contract)), m_context(context), m_options(options) + : Sink("MqttService", std::move(contract)), + m_context(context), + m_strand(context), + m_options(options), + m_currentTimer(context) { + // Unique id number for agent instance + m_instanceId = getCurrentTimeInSec(); + auto jsonPrinter = dynamic_cast(m_sinkContract->getPrinter("json")); + m_jsonPrinter = make_unique(jsonPrinter->getJsonVersion()); + m_printer = std::make_unique(jsonPrinter->getJsonVersion()); + GetOptions(config, m_options, options); AddOptions(config, m_options, {{configuration::ProbeTopic, string()}, {configuration::MqttCaCert, string()}, {configuration::MqttPrivateKey, string()}, {configuration::MqttCert, string()}, + {configuration::MqttClientId, string()}, {configuration::MqttUserName, string()}, - {configuration::MqttPassword, string()}, - {configuration::MqttClientId, string()}}); - AddDefaultedOptions(config, m_options, - {{configuration::MqttHost, "127.0.0.1"s}, - {configuration::DeviceTopic, "MTConnect/Device/"s}, - {configuration::AssetTopic, "MTConnect/Asset/"s}, - {configuration::ObservationTopic, "MTConnect/Observation/"s}, - {configuration::MqttPort, 1883}, - {configuration::MqttTls, false}}); - - auto clientHandler = make_unique(); - clientHandler->m_connected = [this](shared_ptr client) { - // Publish latest devices, assets, and observations - auto &circ = m_sinkContract->getCircularBuffer(); - std::lock_guard lock(circ); - client->connectComplete(); - - for (auto &dev : m_sinkContract->getDevices()) + {configuration::MqttPassword, string()}}); + AddDefaultedOptions( + config, m_options, + {{configuration::MqttHost, "127.0.0.1"s}, + {configuration::DeviceTopic, "MTConnect/Probe/[device]"s}, + {configuration::AssetTopic, "MTConnect/Asset/[device]"s}, + {configuration::MqttLastWillTopic, "MTConnect/Probe/[device]/Availability"s}, + {configuration::CurrentTopic, "MTConnect/Current/[device]"s}, + {configuration::SampleTopic, "MTConnect/Sample/[device]"s}, + {configuration::MqttCurrentInterval, 10000ms}, + {configuration::MqttSampleInterval, 500ms}, + {configuration::MqttSampleCount, 1000}, + {configuration::MqttPort, 1883}, + {configuration::MqttTls, false}}); + + int maxTopicDepth {GetOption(options, configuration::MqttMaxTopicDepth).value_or(7)}; + + m_deviceTopic = GetOption(m_options, configuration::ProbeTopic) + .value_or(get(m_options[configuration::DeviceTopic])); + m_assetTopic = getTopic(configuration::AssetTopic, maxTopicDepth); + m_currentTopic = getTopic(configuration::CurrentTopic, maxTopicDepth); + m_sampleTopic = getTopic(configuration::SampleTopic, maxTopicDepth); + + m_currentInterval = *GetOption(m_options, configuration::MqttCurrentInterval); + m_sampleInterval = *GetOption(m_options, configuration::MqttSampleInterval); + + m_sampleCount = *GetOption(m_options, configuration::MqttSampleCount); + } + + void MqttService::start() + { + if (!m_client) + { + auto clientHandler = make_unique(); + clientHandler->m_connected = [this](shared_ptr client) { + // Publish latest devices, assets, and observations + auto &circ = m_sinkContract->getCircularBuffer(); + std::lock_guard lock(circ); + client->connectComplete(); + + client->publish(m_lastWillTopic, "AVAILABLE"); + pubishInitialContent(); + }; + + auto agentDevice = m_sinkContract->getDeviceByName("Agent"); + auto lwtTopic = get(m_options[configuration::MqttLastWillTopic]); + m_lastWillTopic = formatTopic(lwtTopic, agentDevice, "Agent"); + + if (IsOptionSet(m_options, configuration::MqttTls)) { - publish(dev); + m_client = make_shared(m_context, m_options, std::move(clientHandler), + m_lastWillTopic, "UNAVAILABLE"s); } - - auto obsList {circ.getLatest().getObservations()}; - for (auto &obs : obsList) + else { - observation::ObservationPtr p {obs.second}; - publish(p); + m_client = make_shared(m_context, m_options, std::move(clientHandler), + m_lastWillTopic, "UNAVAILABLE"s); } + } + m_client->start(); + } + + void MqttService::stop() + { + // stop client side + if (m_client) + m_client->stop(); + + m_currentTimer.cancel(); + } + + struct AsyncSample : public observation::AsyncObserver + { + AsyncSample(boost::asio::io_context::strand &strand, + mtconnect::buffer::CircularBuffer &buffer, FilterSet &&filter, + std::chrono::milliseconds interval, std::chrono::milliseconds heartbeat, + std::shared_ptr client, DevicePtr device) + : observation::AsyncObserver(strand, buffer, std::move(filter), interval, heartbeat), + m_device(device), + m_client(client) + {} + + void fail(boost::beast::http::status status, const std::string &message) override + { + LOG(error) << "MQTT Sample Failed: " << message; + } + + bool isRunning() override + { + if (m_sink.expired()) + return false; + + auto client = m_client.lock(); + return client && client->isRunning() && client->isConnected(); + } + + bool cancel() override { return true; } + + DevicePtr m_device; + std::weak_ptr m_client; + std::weak_ptr + m_sink; //! weak shared pointer to the sink. handles shutdown timer race + }; + + void MqttService::pubishInitialContent() + { + using std::placeholders::_1; + for (auto &dev : m_sinkContract->getDevices()) + { + publish(dev); AssetList list; - m_sinkContract->getAssetStorage()->getAssets(list, 100000); + m_sinkContract->getAssetStorage()->getAssets(list, 100000, true, *(dev->getUuid())); for (auto &asset : list) { publish(asset); } - }; - - m_devicePrefix = GetOption(m_options, configuration::ProbeTopic) - .value_or(get(m_options[configuration::DeviceTopic])); - m_assetPrefix = get(m_options[configuration::AssetTopic]); - m_observationPrefix = get(m_options[configuration::ObservationTopic]); - - if (IsOptionSet(m_options, configuration::MqttTls)) - { - m_client = make_shared(m_context, m_options, std::move(clientHandler)); } - else + + auto seq = m_sinkContract->getCircularBuffer().getSequence(); + for (auto &dev : m_sinkContract->getDevices()) { - m_client = make_shared(m_context, m_options, std::move(clientHandler)); + FilterSet filterSet = filterForDevice(dev); + auto sampler = + make_shared(m_strand, m_sinkContract->getCircularBuffer(), + std::move(filterSet), m_sampleInterval, 600s, m_client, dev); + sampler->m_sink = getptr(); + sampler->m_handler = boost::bind(&MqttService::publishSample, this, _1); + sampler->observe(seq, [this](const std::string &id) { + return m_sinkContract->getDataItemById(id).get(); + }); + sampler->handlerCompleted(); } + + publishCurrent(boost::system::error_code {}); } - void MqttService::start() + /// @brief publish sample when observations arrive. + SequenceNumber_t MqttService::publishSample( + std::shared_ptr observer) { - // mqtt client side not a server side... - if (!m_client) - return; + auto sampler = std::dynamic_pointer_cast(observer); + auto topic = formatTopic(m_sampleTopic, sampler->m_device); + LOG(debug) << "Publishing sample for: " << topic; - m_client->start(); - } + std::unique_ptr observations; + SequenceNumber_t end {0}; + std::string doc; + SequenceNumber_t firstSeq, lastSeq; - void MqttService::stop() - { - // stop client side - if (m_client) - m_client->stop(); - } + { + auto &buffer = m_sinkContract->getCircularBuffer(); + std::lock_guard lock(buffer); - std::shared_ptr MqttService::getClient() { return m_client; } + lastSeq = buffer.getSequence() - 1; + observations = + buffer.getObservations(m_sampleCount, sampler->getFilter(), sampler->getSequence(), + nullopt, end, firstSeq, observer->m_endOfBuffer); + } - bool MqttService::publish(observation::ObservationPtr &observation) - { - // get the data item from observation - if (observation->isOrphan()) - return false; + doc = m_printer->printSample(m_instanceId, + m_sinkContract->getCircularBuffer().getBufferSize(), end, + firstSeq, lastSeq, *observations, false); - DataItemPtr dataItem = observation->getDataItem(); + m_client->asyncPublish(topic, doc, [sampler, topic](std::error_code ec) { + if (!ec) + { + sampler->handlerCompleted(); + } + else + { + LOG(warning) << "Async publish failed for " << topic << ": " << ec.message(); + } + }); - auto topic = m_observationPrefix + dataItem->getTopic(); // client asyn topic - auto content = dataItem->getTopicName(); // client asyn content + return end; + } - // We may want to use the observation from the checkpoint. - string doc; - if (observation->getDataItem()->isCondition()) + void MqttService::publishCurrent(boost::system::error_code ec) + { + if (ec) { - doc = m_jsonPrinter->print(observation); + LOG(warning) << "Mqtt2Service::publishCurrent: " << ec.message(); + return; } - else + + if (!m_client->isRunning() || !m_client->isConnected()) { - doc = m_jsonPrinter->printEntity(observation); + LOG(warning) << "Mqtt2Service::publishCurrent: client stopped"; + return; } - if (m_client) + for (auto &device : m_sinkContract->getDevices()) + { + auto topic = formatTopic(m_currentTopic, device); + LOG(debug) << "Publishing current for: " << topic; + + ObservationList observations; + SequenceNumber_t firstSeq, seq; + auto filterSet = filterForDevice(device); + + { + auto &buffer = m_sinkContract->getCircularBuffer(); + std::lock_guard lock(buffer); + + firstSeq = buffer.getFirstSequence(); + seq = buffer.getSequence(); + m_sinkContract->getCircularBuffer().getLatest().getObservations(observations, + filterSet); + } + + auto doc = m_printer->printSample(m_instanceId, + m_sinkContract->getCircularBuffer().getBufferSize(), + seq, firstSeq, seq - 1, observations); + m_client->publish(topic, doc); + } + + using std::placeholders::_1; + m_currentTimer.expires_after(m_currentInterval); + m_currentTimer.async_wait(boost::asio::bind_executor( + m_strand, boost::bind(&MqttService::publishCurrent, this, _1))); + } + bool MqttService::publish(observation::ObservationPtr &observation) + { + // Since we are doing periodic publishing, there is nothing to do here. return true; } bool MqttService::publish(device_model::DevicePtr device) { - auto topic = m_devicePrefix + *device->getUuid(); + m_filters.clear(); + + auto topic = formatTopic(m_deviceTopic, device); auto doc = m_jsonPrinter->print(device); stringstream buffer; @@ -167,7 +316,17 @@ namespace mtconnect { bool MqttService::publish(asset::AssetPtr asset) { - auto topic = m_assetPrefix + get(asset->getIdentity()); + auto uuid = asset->getDeviceUuid(); + DevicePtr dev; + if (uuid) + dev = m_sinkContract->findDeviceByUUIDorName(*uuid); + auto topic = formatTopic(m_assetTopic, dev); + if (topic.back() != '/') + topic.append("/"); + topic.append(asset->getAssetId()); + + LOG(debug) << "Publishing Asset to topic: " << topic; + auto doc = m_jsonPrinter->print(asset); stringstream buffer; diff --git a/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp b/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp index e94d01f5..6cba5240 100644 --- a/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp +++ b/src/mtconnect/sink/mqtt_sink/mqtt_service.hpp @@ -1,5 +1,5 @@ // -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// Copyright Copyright 2009-2023, AMT – The Association For Manufacturing Technology (“AMT”) // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,21 +20,27 @@ #include "boost/asio/io_context.hpp" #include +#include + #include "mtconnect/buffer/checkpoint.hpp" #include "mtconnect/config.hpp" #include "mtconnect/configuration/agent_config.hpp" #include "mtconnect/entity/json_printer.hpp" #include "mtconnect/mqtt/mqtt_client.hpp" #include "mtconnect/observation/observation.hpp" +#include "mtconnect/printer//json_printer.hpp" #include "mtconnect/printer/printer.hpp" #include "mtconnect/printer/xml_printer_helper.hpp" #include "mtconnect/sink/sink.hpp" #include "mtconnect/utilities.hpp" using namespace std; +using namespace mtconnect; using namespace mtconnect::entity; using namespace mtconnect::mqtt_client; +using json = nlohmann::json; + namespace mtconnect { class XmlPrinter; @@ -43,6 +49,9 @@ namespace mtconnect { /// @brief MTConnect Mqtt implemention namespace namespace mqtt_sink { + + struct AsyncSample; + class AGENT_LIB_API MqttService : public sink::Sink { // dynamic loading of sink @@ -66,6 +75,9 @@ namespace mtconnect { void stop() override; /// @brief Receive an observation + /// + /// This does nothing since we are periodically publishing current and samples + /// /// @param observation shared pointer to the observation /// @return `true` if the publishing was successful bool publish(observation::ObservationPtr &observation) override; @@ -80,27 +92,113 @@ namespace mtconnect { /// @return `true` if successful bool publish(device_model::DevicePtr device) override; + /// @brief Publsh all devices, assets, and begin async timer-based publishing + void pubishInitialContent(); + + /// @brief Publish a current using `CurrentInterval` option. + void publishCurrent(boost::system::error_code ec); + + /// @brief publish sample when observations arrive. + SequenceNumber_t publishSample(std::shared_ptr sampler); + /// @brief Register the Sink factory to create this sink /// @param factory static void registerFactory(SinkFactory &factory); /// @brief gets a Mqtt Client /// @return MqttClient - std::shared_ptr getClient(); + std::shared_ptr getClient() { return m_client; } /// @brief Mqtt Client is Connected or not /// @return `true` when the client was connected bool isConnected() { return m_client && m_client->isConnected(); } protected: - std::string m_devicePrefix; - std::string m_assetPrefix; - std::string m_observationPrefix; + const FilterSet &filterForDevice(const DevicePtr &device) + { + auto filter = m_filters.find(*(device->getUuid())); + if (filter == m_filters.end()) + { + auto pos = m_filters.emplace(*(device->getUuid()), FilterSet()); + filter = pos.first; + auto &set = filter->second; + for (const auto &wdi : device->getDeviceDataItems()) + { + const auto di = wdi.lock(); + if (di) + set.insert(di->getId()); + } + } + return filter->second; + } + + std::string formatTopic(const std::string &topic, const DevicePtr device, + const std::string defaultUuid = "Unknown") + { + string uuid; + string formatted {topic}; + if (!device) + uuid = defaultUuid; + else + { + uuid = *(device->getUuid()); + if (std::dynamic_pointer_cast(device)) + { + uuid.insert(0, "Agent_"); + } + } + + if (formatted.find("[device]") == std::string::npos) + { + if (formatted.back() != '/') + formatted.append("/"); + formatted.append(uuid); + } + else + { + boost::replace_all(formatted, "[device]", uuid); + } + return formatted; + } + + std::string getTopic(const std::string &option, int maxTopicDepth) + { + auto topic {get(m_options[option])}; + auto depth = std::count(topic.begin(), topic.end(), '/'); + + if (depth > maxTopicDepth) + LOG(warning) << "Mqtt Option " << option + << " exceeds maximum number of levels: " << maxTopicDepth; + + return topic; + } + + protected: + std::string m_deviceTopic; //! Device topic prefix + std::string m_assetTopic; //! Asset topic prefix + std::string m_currentTopic; //! Current topic prefix + std::string m_sampleTopic; //! Sample topic prefix + std::string m_lastWillTopic; //! Topic to publish the last will when disconnected + + std::chrono::milliseconds m_currentInterval; //! Interval in ms to update current + std::chrono::milliseconds m_sampleInterval; //! min interval in ms to update sample + + uint64_t m_instanceId; boost::asio::io_context &m_context; + boost::asio::io_context::strand m_strand; + ConfigOptions m_options; + std::unique_ptr m_jsonPrinter; + std::unique_ptr m_printer; + std::shared_ptr m_client; + boost::asio::steady_timer m_currentTimer; + int m_sampleCount; //! Timer for current requests + + std::map m_filters; + std::map> m_samplers; }; } // namespace mqtt_sink } // namespace sink diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index ebff0602..40eb1217 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -57,6 +57,9 @@ namespace mtconnect::sink::rest_sink { /// The request can be a simple reply response or streaming request struct Request { + Request() = default; + Request(const Request &request) = default; + boost::beast::http::verb m_verb; ///< GET, PUT, POST, or DELETE std::string m_body; ///< The body of the request std::string m_accepts; ///< The accepts header @@ -68,6 +71,9 @@ namespace mtconnect::sink::rest_sink { QueryMap m_query; ///< The parsed query parameters ParameterMap m_parameters; ///< The parsed path parameters + std::optional m_requestId; ///< Request id from websocket sub + std::optional m_command; ///< Specific request from websocket + /// @brief Find a parameter by type /// @tparam T the type of the parameter /// @param s the name of the parameter diff --git a/src/mtconnect/sink/rest_sink/response.hpp b/src/mtconnect/sink/rest_sink/response.hpp index a67bdbe6..b19fecff 100644 --- a/src/mtconnect/sink/rest_sink/response.hpp +++ b/src/mtconnect/sink/rest_sink/response.hpp @@ -64,6 +64,7 @@ namespace mtconnect { std::chrono::seconds m_expires; ///< how long should this session should stay open before it is closed bool m_close {false}; ///< `true` if this session should closed after it responds + std::optional m_requestId; ///< Request id from websocket sub CachedFilePtr m_file; ///< Cached file if a file is being returned }; diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 9237a464..21086547 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -113,8 +113,10 @@ namespace mtconnect { {"from", QUERY, "Sequence number at to start reporting observations"}, {"interval", QUERY, "Time in ms between publishing data–starts streaming"}, {"pretty", QUERY, "Instructs the result to be pretty printed"}, + {"format", QUERY, "The format of the response document: 'xml' or 'json'"}, {"heartbeat", QUERY, - "Time in ms between publishing a empty document when no data has changed"}}); + "Time in ms between publishing a empty document when no data has changed"}, + {"id", PATH, "webservice request id"}}); createProbeRoutings(); createCurrentRoutings(); @@ -122,6 +124,7 @@ namespace mtconnect { createAssetRoutings(); createPutObservationRoutings(); createFileRoutings(); + m_server->addCommands(); makeLoopbackSource(m_sinkContract->m_pipelineContext); } @@ -434,8 +437,10 @@ namespace mtconnect { // Request Routings // ----------------------------------------------------------- - static inline void respond(rest_sink::SessionPtr session, rest_sink::ResponsePtr &&response) + static inline void respond(rest_sink::SessionPtr session, rest_sink::ResponsePtr &&response, + std::optional id = std::nullopt) { + response->m_requestId = id; session->writeResponse(std::move(response)); } @@ -472,8 +477,8 @@ namespace mtconnect { auto device = request->parameter("device"); auto pretty = *request->parameter("pretty"); auto deviceType = request->parameter("deviceType"); - - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); if (device && !ends_with(request->m_path, string("probe")) && m_sinkContract->findDeviceByUUIDorName(*device) == nullptr) @@ -484,33 +489,37 @@ namespace mtconnect { return false; } - respond(session, probeRequest(printer, device, pretty, deviceType)); + respond(session, probeRequest(printer, device, pretty, deviceType, request->m_requestId), + request->m_requestId); return true; }; m_server ->addRouting({boost::beast::http::verb::get, - "/probe?pretty={bool:false}&deviceType={string}", handler}) + "/probe?pretty={bool:false}&deviceType={string}&format={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); m_server ->addRouting({boost::beast::http::verb::get, - "/{device}/probe?pretty={bool:false}&deviceType={string}", handler}) + "/{device}/probe?pretty={bool:false}&deviceType={string}&format={string}", + handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " - "device identified by `device` matching `name` or `uuid`."); + "device identified by `device` matching `name` or `uuid`.") + .command("probe"); // Must be last m_server - ->addRouting( - {boost::beast::http::verb::get, "/?pretty={bool:false}&deviceType={string}", handler}) + ->addRouting({boost::beast::http::verb::get, + "/?pretty={bool:false}&deviceType={string}&format={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); m_server ->addRouting({boost::beast::http::verb::get, - "/{device}?pretty={bool:false}&deviceType={string}", handler}) + "/{device}?pretty={bool:false}&deviceType={string}&format={string}", + handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " "device identified by `device` matching `name` or `uuid`."); @@ -522,10 +531,14 @@ namespace mtconnect { auto handler = [&](SessionPtr session, RequestPtr request) -> bool { auto removed = *request->parameter("removed"); auto count = *request->parameter("count"); - auto printer = printerForAccepts(request->m_accepts); - - respond(session, assetRequest(printer, count, removed, request->parameter("type"), - request->parameter("device"))); + auto pretty = request->parameter("pretty").value_or(false); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + + respond(session, + assetRequest(printer, count, removed, request->parameter("type"), + request->parameter("device"), pretty, request->m_requestId), + request->m_requestId); return true; }; @@ -533,6 +546,7 @@ namespace mtconnect { auto asset = request->parameter("assetIds"); if (asset) { + auto pretty = request->parameter("pretty").value_or(false); auto printer = m_sinkContract->getPrinter(acceptFormat(request->m_accepts)); list ids; @@ -540,21 +554,24 @@ namespace mtconnect { string id; while (getline(str, id, ';')) ids.emplace_back(id); - respond(session, assetIdsRequest(printer, ids)); + respond(session, assetIdsRequest(printer, ids, pretty, request->m_requestId), + request->m_requestId); } else { - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); auto error = printError(printer, "INVALID_REQUEST", "No asset given"); - respond(session, make_unique(rest_sink::status::bad_request, error, - printer->mimeType())); + respond(session, + make_unique(rest_sink::status::bad_request, error, printer->mimeType()), + request->m_requestId); } return true; }; string qp( "type={string}&removed={bool:false}&" - "count={integer:100}&device={string}&pretty={bool:false}"); + "count={integer:100}&device={string}&pretty={bool:false}&format={string}"); m_server->addRouting({boost::beast::http::verb::get, "/assets?" + qp, handler}) .document("MTConnect assets request", "Returns up to `count` assets"); m_server->addRouting({boost::beast::http::verb::get, "/asset?" + qp, handler}) @@ -575,11 +592,13 @@ namespace mtconnect { if (m_server->arePutsAllowed()) { auto putHandler = [&](SessionPtr session, RequestPtr request) -> bool { - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); respond(session, putAssetRequest(printer, request->m_body, request->parameter("type"), request->parameter("device"), - request->parameter("assetId"))); + request->parameter("assetId")), + request->m_requestId); return true; }; @@ -590,17 +609,22 @@ namespace mtconnect { list ids; stringstream str(*asset); string id; - auto printer = printerForAccepts(request->m_accepts); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); while (getline(str, id, ';')) ids.emplace_back(id); - respond(session, deleteAssetRequest(printer, ids)); + respond(session, deleteAssetRequest(printer, ids), request->m_requestId); } else { - respond(session, deleteAllAssetsRequest(printerForAccepts(request->m_accepts), - request->parameter("device"), - request->parameter("type"))); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + + respond(session, + deleteAllAssetsRequest(printer, request->parameter("device"), + request->parameter("type")), + request->m_requestId); } return true; }; @@ -612,37 +636,47 @@ namespace mtconnect { { m_server ->addRouting( - {t, "/" + asset + "/{assetId}?device={string}&type={string}", putHandler}) + {t, "/" + asset + "/{assetId}?device={string}&type={string}&format={string}", + putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); - m_server->addRouting({t, "/" + asset + "?device={string}&type={string}", putHandler}) + m_server + ->addRouting( + {t, "/" + asset + "?device={string}&type={string}&format={string}", putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); - m_server->addRouting({t, "/{device}/" + asset + "/{assetId}?type={string}", putHandler}) + m_server + ->addRouting({t, "/{device}/" + asset + "/{assetId}?type={string}&format={string}", + putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); - m_server->addRouting({t, "/{device}/" + asset + "?type={string}", putHandler}) + m_server + ->addRouting( + {t, "/{device}/" + asset + "?type={string}&format={string}", putHandler}) .document("Upload an asset by identified by `assetId`", "Updates or adds an asset with the asset XML in the body"); } m_server ->addRouting({boost::beast::http::verb::delete_, - "/" + asset + "?device={string}&type={string}", deleteHandler}) + "/" + asset + "?device={string}&type={string}&format={string}", + deleteHandler}) .document("Delete all assets for a device and type", "Device and type are optional. If they are not given, it assumes there is " "no constraint"); m_server - ->addRouting( - {boost::beast::http::verb::delete_, "/" + asset + "/{assetId}", deleteHandler}) + ->addRouting({boost::beast::http::verb::delete_, + "/" + asset + "/{assetId}?format={string}", deleteHandler}) .document("Delete asset identified by `assetId`", "Marks the asset as removed and creates an AssetRemoved event"); m_server ->addRouting({boost::beast::http::verb::delete_, - "/{device}/" + asset + "?type={string}", deleteHandler}) + "/{device}/" + asset + "?type={string}&format={string}", deleteHandler}) .document("Delete all assets for a device and type", "Device and type are optional. If they are not given, it assumes there is " - "no constraint"); + "no constraint") + .command("asset"); + ; } } } @@ -654,19 +688,26 @@ namespace mtconnect { auto interval = request->parameter("interval"); if (interval) { - streamCurrentRequest( - session, printerForAccepts(request->m_accepts), *interval, - request->parameter("device"), request->parameter("path"), - *request->parameter("pretty"), request->parameter("deviceType")); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + + streamCurrentRequest(session, printer, *interval, request->parameter("device"), + request->parameter("path"), + *request->parameter("pretty"), + request->parameter("deviceType"), request->m_requestId); } else { - respond(session, currentRequest(printerForAccepts(request->m_accepts), - request->parameter("device"), - request->parameter("at"), - request->parameter("path"), - *request->parameter("pretty"), - request->parameter("deviceType"))); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + + respond( + session, + currentRequest(printer, request->parameter("device"), + request->parameter("at"), request->parameter("path"), + *request->parameter("pretty"), + request->parameter("deviceType"), request->m_requestId), + request->m_requestId); } return true; }; @@ -674,7 +715,7 @@ namespace mtconnect { string qp( "path={string}&at={unsigned_integer}&" "interval={integer}&pretty={bool:false}&" - "deviceType={string}"); + "deviceType={string}&format={string}"); m_server->addRouting({boost::beast::http::verb::get, "/current?" + qp, handler}) .document("MTConnect current request", "Gets a stapshot of the state of all the observations for all devices " @@ -682,7 +723,8 @@ namespace mtconnect { m_server->addRouting({boost::beast::http::verb::get, "/{device}/current?" + qp, handler}) .document("MTConnect current request", "Gets a stapshot of the state of all the observations for device `device` " - "optionally filtered by the `path`"); + "optionally filtered by the `path`") + .command("current"); } void RestService::createSampleRoutings() @@ -692,32 +734,57 @@ namespace mtconnect { auto interval = request->parameter("interval"); if (interval) { + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + streamSampleRequest( - session, printerForAccepts(request->m_accepts), *interval, - *request->parameter("heartbeat"), *request->parameter("count"), - request->parameter("device"), request->parameter("from"), - request->parameter("path"), *request->parameter("pretty"), - request->parameter("deviceType")); + session, printer, *interval, *request->parameter("heartbeat"), + *request->parameter("count"), request->parameter("device"), + request->parameter("from"), request->parameter("path"), + *request->parameter("pretty"), request->parameter("deviceType"), + request->m_requestId); } else { - respond( - session, - sampleRequest( - printerForAccepts(request->m_accepts), *request->parameter("count"), - request->parameter("device"), request->parameter("from"), - request->parameter("to"), request->parameter("path"), - *request->parameter("pretty"), request->parameter("deviceType"))); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); + + respond(session, + sampleRequest( + printer, *request->parameter("count"), + request->parameter("device"), request->parameter("from"), + request->parameter("to"), request->parameter("path"), + *request->parameter("pretty"), request->parameter("deviceType"), + request->m_requestId), + request->m_requestId); } return true; }; + auto cancelHandler = [&](SessionPtr session, RequestPtr request) -> bool { + if (request->m_requestId) + { + auto requestId = *request->m_requestId; + auto success = session->cancelRequest(requestId); + auto response = make_unique( + status::ok, "{ \"success\": \""s + (success ? "true" : "false") + "\"}", + "application/json"); + + respond(session, std::move(response), request->m_requestId); + return true; + } + else + { + return false; + } + }; + string qp( "path={string}&from={unsigned_integer}&" "interval={integer}&count={integer:100}&" "heartbeat={integer:10000}&to={unsigned_integer}&" "pretty={bool:false}&" - "deviceType={string}"); + "deviceType={string}&format={string}"); m_server->addRouting({boost::beast::http::verb::get, "/sample?" + qp, handler}) .document("MTConnect sample request", "Gets a time series of at maximum `count` observations for all devices " @@ -727,7 +794,11 @@ namespace mtconnect { .document("MTConnect sample request", "Gets a time series of at maximum `count` observations for device `device` " "optionally filtered by the `path` and starting at `from`. By default, from is " - "the first available observation known to the agent"); + "the first available observation known to the agent") + .command("sample"); + m_server->addRouting({boost::beast::http::verb::get, "/cancel/id={string}", cancelHandler}) + .document("MTConnect WebServices Cancel Stream", "Cancels a streaming sample request") + .command("cancel"); } void RestService::createPutObservationRoutings() @@ -744,9 +815,11 @@ namespace mtconnect { if (ts) queries.erase("time"); auto device = request->parameter("device"); + auto format = request->parameter("format"); + auto printer = getPrinter(request->m_accepts, format); - respond(session, putObservationRequest(printerForAccepts(request->m_accepts), *device, - queries, ts)); + respond(session, putObservationRequest(printer, *device, queries, ts), + request->m_requestId); return true; } else @@ -776,7 +849,8 @@ namespace mtconnect { ResponsePtr RestService::probeRequest(const Printer *printer, const std::optional &device, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { NAMED_SCOPE("RestService::probeRequest"); @@ -805,7 +879,7 @@ namespace mtconnect { m_sinkContract->getCircularBuffer().getSequence(), uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), uint32_t(m_sinkContract->getAssetStorage()->getCount()), deviceList, - &counts, false, pretty), + &counts, false, pretty, requestId), printer->mimeType()); } @@ -813,7 +887,8 @@ namespace mtconnect { const std::optional &device, const std::optional &at, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { using namespace rest_sink; DevicePtr dev {nullptr}; @@ -830,7 +905,7 @@ namespace mtconnect { // Check if there is a frequency to stream data or not return make_unique(rest_sink::status::ok, - fetchCurrentData(printer, filter, at, pretty), + fetchCurrentData(printer, filter, at, pretty, requestId), printer->mimeType()); } @@ -839,7 +914,8 @@ namespace mtconnect { const std::optional &from, const std::optional &to, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { using namespace rest_sink; DevicePtr dev {nullptr}; @@ -860,7 +936,7 @@ namespace mtconnect { return make_unique( rest_sink::status::ok, - fetchSampleData(printer, filter, count, from, to, end, endOfBuffer, pretty), + fetchSampleData(printer, filter, count, from, to, end, endOfBuffer, pretty, requestId), printer->mimeType()); } @@ -880,6 +956,7 @@ namespace mtconnect { if (sink && isRunning()) { m_session->fail(status, message); + cancel(); } else { @@ -906,6 +983,13 @@ namespace mtconnect { } } + bool cancel() override + { + observation::AsyncObserver::cancel(); + m_session.reset(); + return true; + } + std::weak_ptr m_sink; //! weak shared pointer to the sink. handles shutdown timer race int m_count {0}; @@ -921,7 +1005,8 @@ namespace mtconnect { const int count, const std::optional &device, const std::optional &from, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { NAMED_SCOPE("RestService::streamSampleRequest"); @@ -955,6 +1040,8 @@ namespace mtconnect { asyncResponse->m_printer = printer; asyncResponse->m_sink = getptr(); asyncResponse->m_pretty = pretty; + asyncResponse->setRequestId(requestId); + session->addObserver(asyncResponse); if (m_logStreamData) { @@ -972,7 +1059,8 @@ namespace mtconnect { session->beginStreaming( printer->mimeType(), asio::bind_executor(m_strand, - boost::bind(&AsyncObserver::handlerCompleted, asyncResponse))); + boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), + requestId); } SequenceNumber_t RestService::streamNextSampleChunk( @@ -996,15 +1084,20 @@ namespace mtconnect { string content = fetchSampleData(asyncResponse->m_printer, asyncResponse->getFilter(), asyncResponse->m_count, from, nullopt, end, - asyncObserver->m_endOfBuffer, asyncResponse->m_pretty); + asyncObserver->m_endOfBuffer, asyncResponse->m_pretty, + asyncResponse->getRequestId()); if (m_logStreamData) asyncResponse->m_log << content << endl; - asyncResponse->m_session->writeChunk( - content, asio::bind_executor( - m_strand, boost::bind(&AsyncObserver::handlerCompleted, asyncResponse))); - + if (asyncResponse->m_session) + { + asyncResponse->m_session->writeChunk( + content, + asio::bind_executor(m_strand, + boost::bind(&AsyncObserver::handlerCompleted, asyncResponse)), + asyncResponse->getRequestId()); + } return end; } @@ -1012,9 +1105,12 @@ namespace mtconnect { { LOG(error) << asyncResponse->m_session->getRemote().address() << ": Error processing request: " << re.what(); - ResponsePtr resp = std::make_unique(re); - asyncResponse->m_session->writeResponse(std::move(resp)); - asyncResponse->m_session->close(); + if (asyncResponse->m_session) + { + ResponsePtr resp = std::make_unique(re); + asyncResponse->m_session->writeResponse(std::move(resp)); + asyncResponse->m_session->close(); + } } catch (...) @@ -1028,15 +1124,26 @@ namespace mtconnect { return 0; } - struct AsyncCurrentResponse + struct AsyncCurrentResponse : public AsyncResponse { - AsyncCurrentResponse(rest_sink::SessionPtr session, asio::io_context &context) - : m_session(session), m_timer(context) + AsyncCurrentResponse(rest_sink::SessionPtr session, asio::io_context &context, + chrono::milliseconds interval) + : AsyncResponse(interval), m_session(session), m_timer(context) {} + auto getptr() { return dynamic_pointer_cast(shared_from_this()); } + + bool cancel() override + { + m_timer.cancel(); + m_session.reset(); + return true; + } + + bool isRunning() override { return (bool)m_session; } + std::weak_ptr m_service; rest_sink::SessionPtr m_session; - chrono::milliseconds m_interval; const Printer *m_printer {nullptr}; FilterSetOpt m_filter; boost::asio::steady_timer m_timer; @@ -1047,7 +1154,8 @@ namespace mtconnect { const int interval, const std::optional &device, const std::optional &path, bool pretty, - const std::optional &deviceType) + const std::optional &deviceType, + const std::optional &requestId) { checkRange(printer, interval, 0, numeric_limits().max(), "interval"); DevicePtr dev {nullptr}; @@ -1056,21 +1164,27 @@ namespace mtconnect { dev = checkDevice(printer, *device); } - auto asyncResponse = make_shared(session, m_context); + auto asyncResponse = + make_shared(session, m_context, chrono::milliseconds {interval}); if (path || device || deviceType) { asyncResponse->m_filter = make_optional(); checkPath(printer, path, dev, *asyncResponse->m_filter, deviceType); } - asyncResponse->m_interval = chrono::milliseconds {interval}; asyncResponse->m_printer = printer; asyncResponse->m_service = getptr(); asyncResponse->m_pretty = pretty; + asyncResponse->setRequestId(requestId); + session->addObserver(asyncResponse); asyncResponse->m_session->beginStreaming( - printer->mimeType(), boost::asio::bind_executor(m_strand, [this, asyncResponse]() { - streamNextCurrent(asyncResponse, boost::system::error_code {}); - })); + printer->mimeType(), + boost::asio::bind_executor(m_strand, + [this, asyncResponse]() { + streamNextCurrent(asyncResponse, + boost::system::error_code {}); + }), + requestId); } void RestService::streamNextCurrent(std::shared_ptr asyncResponse, @@ -1085,7 +1199,7 @@ namespace mtconnect { if (!service || !m_server || !m_server->isRunning()) { LOG(warning) << "Trying to send chunk when service has stopped"; - if (service) + if (service && asyncResponse->m_session) { asyncResponse->m_session->fail(boost::beast::http::status::internal_server_error, "Agent shutting down, aborting stream"); @@ -1097,27 +1211,38 @@ namespace mtconnect { { LOG(warning) << "Unexpected error streamNextCurrent, aborting"; LOG(warning) << ec.category().message(ec.value()) << ": " << ec.message(); - asyncResponse->m_session->fail(boost::beast::http::status::internal_server_error, - "Unexpected error streamNextCurrent, aborting"); + if (asyncResponse->m_session) + asyncResponse->m_session->fail(boost::beast::http::status::internal_server_error, + "Unexpected error streamNextCurrent, aborting"); return; } - asyncResponse->m_session->writeChunk( - fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, - asyncResponse->m_pretty), - boost::asio::bind_executor(m_strand, [this, asyncResponse]() { - asyncResponse->m_timer.expires_from_now(asyncResponse->m_interval); - asyncResponse->m_timer.async_wait(boost::asio::bind_executor( - m_strand, boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); - })); + if (asyncResponse->m_session) + { + asyncResponse->m_session->writeChunk( + fetchCurrentData(asyncResponse->m_printer, asyncResponse->m_filter, nullopt, + asyncResponse->m_pretty, asyncResponse->getRequestId()), + boost::asio::bind_executor( + m_strand, + [this, asyncResponse]() { + asyncResponse->m_timer.expires_from_now(asyncResponse->getInterval()); + asyncResponse->m_timer.async_wait(boost::asio::bind_executor( + m_strand, + boost::bind(&RestService::streamNextCurrent, this, asyncResponse, _1))); + }), + asyncResponse->getRequestId()); + } } catch (RequestError &re) { LOG(error) << asyncResponse->m_session->getRemote().address() << ": Error processing request: " << re.what(); - ResponsePtr resp = std::make_unique(re); - asyncResponse->m_session->writeResponse(std::move(resp)); - asyncResponse->m_session->close(); + if (asyncResponse->m_session) + { + ResponsePtr resp = std::make_unique(re); + asyncResponse->m_session->writeResponse(std::move(resp)); + asyncResponse->m_session->close(); + } } catch (...) @@ -1125,14 +1250,18 @@ namespace mtconnect { std::stringstream txt; txt << asyncResponse->m_session->getRemote().address() << ": Unknown Error thrown"; LOG(error) << txt.str(); - asyncResponse->m_session->fail(boost::beast::http::status::not_found, txt.str()); + if (asyncResponse->m_session) + { + asyncResponse->m_session->fail(boost::beast::http::status::not_found, txt.str()); + } } } ResponsePtr RestService::assetRequest(const Printer *printer, const int32_t count, const bool removed, const std::optional &type, - const std::optional &device, bool pretty) + const std::optional &device, bool pretty, + const std::optional &requestId) { using namespace rest_sink; @@ -1150,12 +1279,13 @@ namespace mtconnect { status::ok, printer->printAssets( m_instanceId, uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), - uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty), + uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty, requestId), printer->mimeType()); } ResponsePtr RestService::assetIdsRequest(const Printer *printer, - const std::list &ids, bool pretty) + const std::list &ids, bool pretty, + const std::optional &requestId) { using namespace rest_sink; @@ -1168,9 +1298,9 @@ namespace mtconnect { str << id << ", "; auto message = str.str().substr(0, str.str().size() - 2); - return make_unique(status::not_found, - printError(printer, "ASSET_NOT_FOUND", message, pretty), - printer->mimeType()); + return make_unique( + status::not_found, printError(printer, "ASSET_NOT_FOUND", message, pretty, requestId), + printer->mimeType()); } else { @@ -1178,7 +1308,7 @@ namespace mtconnect { status::ok, printer->printAssets( m_instanceId, uint32_t(m_sinkContract->getAssetStorage()->getMaxAssets()), - uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty), + uint32_t(m_sinkContract->getAssetStorage()->getCount()), list, pretty, requestId), printer->mimeType()); } } @@ -1358,19 +1488,15 @@ namespace mtconnect { return "xml"; } - const Printer *RestService::printerForAccepts(const std::string &accepts) const - { - return m_sinkContract->getPrinter(acceptFormat(accepts)); - } - string RestService::printError(const Printer *printer, const string &errorCode, - const string &text, bool pretty) const + const string &text, bool pretty, + const std::optional &requestId) const { LOG(debug) << "Returning error " << errorCode << ": " << text; if (printer) return printer->printError( m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty); + m_sinkContract->getCircularBuffer().getSequence(), errorCode, text, pretty, requestId); else return errorCode + ": " + text; } @@ -1446,7 +1572,8 @@ namespace mtconnect { // ------------------------------------------- string RestService::fetchCurrentData(const Printer *printer, const FilterSetOpt &filterSet, - const optional &at, bool pretty) + const optional &at, bool pretty, + const std::optional &requestId) { ObservationList observations; SequenceNumber_t firstSeq, seq; @@ -1470,13 +1597,14 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - seq, firstSeq, seq - 1, observations, pretty); + seq, firstSeq, seq - 1, observations, pretty, requestId); } string RestService::fetchSampleData(const Printer *printer, const FilterSetOpt &filterSet, int count, const std::optional &from, const std::optional &to, - SequenceNumber_t &end, bool &endOfBuffer, bool pretty) + SequenceNumber_t &end, bool &endOfBuffer, bool pretty, + const std::optional &requestId) { std::unique_ptr observations; SequenceNumber_t firstSeq, lastSeq; @@ -1506,7 +1634,7 @@ namespace mtconnect { } return printer->printSample(m_instanceId, m_sinkContract->getCircularBuffer().getBufferSize(), - end, firstSeq, lastSeq, *observations, pretty); + end, firstSeq, lastSeq, *observations, pretty, requestId); } } // namespace sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 51505fd5..f58c7df2 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -101,7 +101,8 @@ namespace mtconnect { ResponsePtr probeRequest(const printer::Printer *p, const std::optional &device = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for a current request /// @param[in] p printer for doc generation @@ -115,7 +116,8 @@ namespace mtconnect { const std::optional &at = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for a sample request /// @param[in] p printer for doc generation @@ -132,7 +134,8 @@ namespace mtconnect { const std::optional &to = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for a streaming sample /// @param[in] session session to stream data to /// @param[in] p printer for doc generation @@ -149,7 +152,8 @@ namespace mtconnect { const std::optional &from = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for a streaming current /// @param[in] session session to stream data to @@ -162,7 +166,8 @@ namespace mtconnect { const std::optional &device = std::nullopt, const std::optional &path = std::nullopt, bool pretty = false, - const std::optional &deviceType = std::nullopt); + const std::optional &deviceType = std::nullopt, + const std::optional &requestId = std::nullopt); /// @brief Handler for put/post observation /// @param[in] p printer for response generation /// @param[in] device device @@ -205,7 +210,8 @@ namespace mtconnect { ResponsePtr assetRequest(const printer::Printer *p, const int32_t count, const bool removed, const std::optional &type = std::nullopt, const std::optional &device = std::nullopt, - bool pretty = false); + bool pretty = false, + const std::optional &requestId = std::nullopt); /// @brief Asset request handler using a list of asset ids /// @param[in] p printer for the response document @@ -213,7 +219,8 @@ namespace mtconnect { /// @param[in] pretty `true` to ensure response is formatted /// @return MTConnect Assets response document ResponsePtr assetIdsRequest(const printer::Printer *p, const std::list &ids, - bool pretty = false); + bool pretty = false, + const std::optional &requestId = std::nullopt); /// @brief Asset request handler to update an asset /// @param p printer for the response document @@ -252,7 +259,26 @@ namespace mtconnect { /// @brief get a printer given a list of formats from the Accepts header /// @param accepts the accepts header /// @return pointer to a printer - const printer::Printer *printerForAccepts(const std::string &accepts) const; + const printer::Printer *printerForAccepts(const std::string &accepts) const + { + return m_sinkContract->getPrinter(acceptFormat(accepts)); + } + + /// @brief get a printer for a format or using the accepts header. Falls back to header accept + /// if format incorrect. + /// @param accepts the accept header of the request + /// @param format optional format query param + /// @return pointer to a printer + const printer::Printer *getPrinter(const std::string &accepts, + std::optional format) const + { + const printer::Printer *printer = nullptr; + if (format) + printer = m_sinkContract->getPrinter(*format); + if (printer == nullptr) + printer = printerForAccepts(accepts); + return printer; + } /// @brief Generate an MTConnect Error document /// @param printer printer to generate error @@ -260,7 +286,8 @@ namespace mtconnect { /// @param text descriptive error text /// @return MTConnect Error document std::string printError(const printer::Printer *printer, const std::string &errorCode, - const std::string &text, bool pretty = false) const; + const std::string &text, bool pretty = false, + const std::optional &requestId = std::nullopt) const; /// @name For testing only ///@{ @@ -299,13 +326,15 @@ namespace mtconnect { // Current Data Collection std::string fetchCurrentData(const printer::Printer *printer, const FilterSetOpt &filterSet, - const std::optional &at, bool pretty = false); + const std::optional &at, bool pretty = false, + const std::optional &requestId = std::nullopt); // Sample data collection std::string fetchSampleData(const printer::Printer *printer, const FilterSetOpt &filterSet, int count, const std::optional &from, const std::optional &to, SequenceNumber_t &end, - bool &endOfBuffer, bool pretty = false); + bool &endOfBuffer, bool pretty = false, + const std::optional &requestId = std::nullopt); // Verification methods template @@ -321,22 +350,15 @@ namespace mtconnect { protected: // Loopback boost::asio::io_context &m_context; - boost::asio::io_context::strand m_strand; - std::string m_schemaVersion; - ConfigOptions m_options; - std::shared_ptr m_loopback; - uint64_t m_instanceId; - std::unique_ptr m_server; // Buffers FileCache m_fileCache; - bool m_logStreamData {false}; }; } // namespace sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index ca8bdf38..bfa584ee 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -53,8 +53,8 @@ namespace mtconnect::sink::rest_sink { /// @param[in] function the function to call if matches /// @param[in] swagger `true` if swagger related Routing(boost::beast::http::verb verb, const std::string &pattern, const Function function, - bool swagger = false) - : m_verb(verb), m_function(function), m_swagger(swagger) + bool swagger = false, std::optional request = std::nullopt) + : m_verb(verb), m_command(request), m_function(function), m_swagger(swagger) { std::string s(pattern); @@ -79,8 +79,12 @@ namespace mtconnect::sink::rest_sink { /// @param[in] function the function to call if matches /// @param[in] swagger `true` if swagger related Routing(boost::beast::http::verb verb, const std::regex &pattern, const Function function, - bool swagger = false) - : m_verb(verb), m_pattern(pattern), m_function(function), m_swagger(swagger) + bool swagger = false, std::optional request = std::nullopt) + : m_verb(verb), + m_pattern(pattern), + m_command(request), + m_function(function), + m_swagger(swagger) {} /// @brief Added summary and description to the routing @@ -165,46 +169,52 @@ namespace mtconnect::sink::rest_sink { { try { - request->m_parameters.clear(); - std::smatch m; - if (m_verb == request->m_verb && std::regex_match(request->m_path, m, m_pattern)) + if (!request->m_command) { - auto s = m.begin(); - s++; - for (auto &p : m_pathParameters) + request->m_parameters.clear(); + std::smatch m; + if (m_verb == request->m_verb && std::regex_match(request->m_path, m, m_pattern)) { - if (s != m.end()) + auto s = m.begin(); + s++; + for (auto &p : m_pathParameters) { - ParameterValue v(s->str()); - request->m_parameters.emplace(make_pair(p.m_name, v)); - s++; + if (s != m.end()) + { + ParameterValue v(s->str()); + request->m_parameters.emplace(make_pair(p.m_name, v)); + s++; + } } } + else + { + return false; + } + } - for (auto &p : m_queryParameters) + for (auto &p : m_queryParameters) + { + auto q = request->m_query.find(p.m_name); + if (q != request->m_query.end()) { - auto q = request->m_query.find(p.m_name); - if (q != request->m_query.end()) + try { - try - { - auto v = convertValue(q->second, p.m_type); - request->m_parameters.emplace(make_pair(p.m_name, v)); - } - catch (ParameterError &e) - { - std::string msg = - std::string("for query parameter '") + p.m_name + "': " + e.what(); - throw ParameterError(msg); - } + auto v = convertValue(q->second, p.m_type); + request->m_parameters.emplace(make_pair(p.m_name, v)); } - else if (!std::holds_alternative(p.m_default)) + catch (ParameterError &e) { - request->m_parameters.emplace(make_pair(p.m_name, p.m_default)); + std::string msg = std::string("for query parameter '") + p.m_name + "': " + e.what(); + throw ParameterError(msg); } } - return m_function(session, request); + else if (!std::holds_alternative(p.m_default)) + { + request->m_parameters.emplace(make_pair(p.m_name, p.m_default)); + } } + return m_function(session, request); } catch (ParameterError &e) @@ -225,6 +235,18 @@ namespace mtconnect::sink::rest_sink { /// @brief Get the routing `verb` const auto &getVerb() const { return m_verb; } + /// @brief Get the optional command associated with the routing + /// @returns optional routing + const auto &getCommand() const { return m_command; } + + /// @brief Sets the command associated with this routing for use with websockets + /// @param command the command + auto &command(const std::string &command) + { + m_command = command; + return *this; + } + protected: void pathParameters(std::string s) { @@ -360,6 +382,7 @@ namespace mtconnect::sink::rest_sink { std::optional m_path; ParameterList m_pathParameters; QuerySet m_queryParameters; + std::optional m_command; Function m_function; std::optional m_summary; diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index d72fd176..740efab8 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -159,16 +159,35 @@ namespace mtconnect::sink::rest_sink { { try { - for (auto &r : m_routings) + if (request->m_command) { - if (r.matches(session, request)) - return true; + auto route = m_commands.find(*request->m_command); + if (route != m_commands.end()) + { + if (route->second->matches(session, request)) + return true; + } + else + { + std::stringstream txt; + txt << session->getRemote().address() + << ": Cannot find handler for command: " << *request->m_command; + session->fail(boost::beast::http::status::not_found, txt.str()); + } + } + else + { + for (auto &r : m_routings) + { + if (r.matches(session, request)) + return true; + } + + std::stringstream txt; + txt << session->getRemote().address() << ": Cannot find handler for: " << request->m_verb + << " " << request->m_path; + session->fail(boost::beast::http::status::not_found, txt.str()); } - - std::stringstream txt; - txt << session->getRemote().address() << ": Cannot find handler for: " << request->m_verb - << " " << request->m_path; - session->fail(boost::beast::http::status::not_found, txt.str()); } catch (RequestError &re) { @@ -217,9 +236,21 @@ namespace mtconnect::sink::rest_sink { auto &route = m_routings.emplace_back(routing); if (m_parameterDocumentation) route.documentParameters(*m_parameterDocumentation); + if (route.getCommand()) + m_commands.emplace(*route.getCommand(), &route); return route; } + /// @brief Setup commands from routings + void addCommands() + { + for (auto &route : m_routings) + { + if (route.getCommand()) + m_commands.emplace(*route.getCommand(), &route); + } + } + /// @brief Add common set of documentation for all rest routings /// @param[in] docs Parameter documentation void addParameterDocumentation(const ParameterDocList &docs) @@ -272,6 +303,7 @@ namespace mtconnect::sink::rest_sink { std::set m_allowPutsFrom; std::list m_routings; + std::map m_commands; std::unique_ptr m_fileCache; ErrorFunction m_errorFunction; FieldList m_fields; diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index c4ed9041..c49a2658 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -25,6 +25,7 @@ #include #include "mtconnect/config.hpp" +#include "mtconnect/observation/change_observer.hpp" #include "routing.hpp" namespace mtconnect::sink::rest_sink { @@ -64,11 +65,13 @@ namespace mtconnect::sink::rest_sink { /// @brief begin streaming data to the client using x-multipart-replace /// @param mimeType the mime type of the response /// @param complete completion callback - virtual void beginStreaming(const std::string &mimeType, Complete complete) = 0; + virtual void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) = 0; /// @brief write a chunk for a streaming session /// @param chunk the chunk to write /// @param complete a completion callback - virtual void writeChunk(const std::string &chunk, Complete complete) = 0; + virtual void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) = 0; /// @brief close the session virtual void close() = 0; /// @brief close the stream @@ -116,6 +119,26 @@ namespace mtconnect::sink::rest_sink { m_unauthorized = true; } + /// @brief Add an observer to the list for cleanup later. + void addObserver(std::weak_ptr observer) + { + m_observers.push_back(observer); + } + + bool cancelRequest(const std::string &requestId) + { + for (auto &obs : m_observers) + { + auto pobs = obs.lock(); + if (pobs && pobs->getRequestId() == requestId) + { + pobs->cancel(); + return true; + } + } + return false; + } + protected: Dispatch m_dispatch; ErrorFunction m_errorFunction; @@ -125,6 +148,7 @@ namespace mtconnect::sink::rest_sink { bool m_allowPuts {false}; std::set m_allowPutsFrom; boost::asio::ip::tcp::endpoint m_remote; + std::list> m_observers; }; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index 43b3cbde..cdd04d2e 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -34,6 +34,7 @@ #include "request.hpp" #include "response.hpp" #include "tls_dector.hpp" +#include "websocket_session.hpp" namespace mtconnect::sink::rest_sink { namespace beast = boost::beast; // from @@ -44,6 +45,7 @@ namespace mtconnect::sink::rest_sink { namespace algo = boost::algorithm; namespace sys = boost::system; namespace ssl = boost::asio::ssl; + namespace ws = boost::beast::websocket; using namespace std; using std::placeholders::_1; @@ -243,7 +245,13 @@ namespace mtconnect::sink::rest_sink { LOG(info) << "ReST Request: From [" << m_request->m_foreignIp << ':' << remote.port() << "]: " << msg.method() << " " << msg.target(); - if (!m_dispatch(shared_ptr(), m_request)) + // Check if this is a websocket upgrade request. If so, begin a websocket session. + if (ws::is_upgrade(msg)) + { + LOG(debug) << "Upgrading to websocket request"; + upgrade(std::move(msg)); + } + else if (!m_dispatch(shared_ptr(), m_request)) { ostringstream txt; txt << "Failed to find handler for " << msg.method() << " " << msg.target(); @@ -279,7 +287,8 @@ namespace mtconnect::sink::rest_sink { } template - void SessionImpl::beginStreaming(const std::string &mimeType, Complete complete) + void SessionImpl::beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId) { NAMED_SCOPE("SessionImpl::beginStreaming"); @@ -313,7 +322,8 @@ namespace mtconnect::sink::rest_sink { } template - void SessionImpl::writeChunk(const std::string &body, Complete complete) + void SessionImpl::writeChunk(const std::string &body, Complete complete, + std::optional requestId) { NAMED_SCOPE("SessionImpl::writeChunk"); @@ -462,6 +472,12 @@ namespace mtconnect::sink::rest_sink { } } + SessionPtr HttpSession::upgradeToWebsocket(RequestMessage &&msg) + { + return std::make_shared(std::move(m_stream), std::move(m_request), + std::move(msg), m_dispatch, m_errorFunction); + } + /// @brief A secure https session class HttpsSession : public SessionImpl { @@ -511,6 +527,17 @@ namespace mtconnect::sink::rest_sink { if (!m_closing) { m_closing = true; + + // Release all references from observers. + for (auto obs : m_observers) + { + auto optr = obs.lock(); + if (optr) + { + optr->cancel(); + } + } + // Set the timeout. beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(30)); @@ -519,6 +546,13 @@ namespace mtconnect::sink::rest_sink { } } + /// @brief Upgrade the current connection to a websocket connection. + SessionPtr upgradeToWebsocket(RequestMessage &&msg) + { + return std::make_shared(std::move(m_stream), std::move(m_request), + std::move(msg), m_dispatch, m_errorFunction); + } + protected: void handshake(beast::error_code ec, size_t bytes_used) { @@ -542,6 +576,13 @@ namespace mtconnect::sink::rest_sink { bool m_closing {false}; }; + template + void SessionImpl::upgrade(RequestMessage &&msg) + { + LOG(debug) << "Upgrading session to websockets"; + derived().upgradeToWebsocket(std::move(msg))->run(); + } + void TlsDector::run() { boost::asio::dispatch( diff --git a/src/mtconnect/sink/rest_sink/session_impl.hpp b/src/mtconnect/sink/rest_sink/session_impl.hpp index 539c3ece..b890a603 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.hpp +++ b/src/mtconnect/sink/rest_sink/session_impl.hpp @@ -35,6 +35,11 @@ namespace mtconnect { } namespace sink::rest_sink { + template + class WebsocketSession; + template + using WebsocketSessionPtr = std::shared_ptr>; + /// @brief A session implementation `Derived` subclass pattern /// @tparam subclass of this class to use the same methods with http or https protocol streams template @@ -61,7 +66,7 @@ namespace mtconnect { return std::dynamic_pointer_cast(shared_from_this()); } /// @brief get this as the `Derived` type - /// @return + /// @return the subclass Derived &derived() { return static_cast(*this); } /// @name Session Interface @@ -69,11 +74,15 @@ namespace mtconnect { void run() override; void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override; void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override; - void beginStreaming(const std::string &mimeType, Complete complete) override; - void writeChunk(const std::string &chunk, Complete complete) override; + void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) override; + void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) override; void closeStream() override; ///@} protected: + using RequestMessage = boost::beast::http::request; + template void addHeaders(const Response &response, T &res); @@ -81,6 +90,7 @@ namespace mtconnect { void sent(boost::system::error_code ec, size_t len); void read(); void reset(); + void upgrade(RequestMessage &&msg); protected: using RequestParser = boost::beast::http::request_parser; @@ -145,6 +155,9 @@ namespace mtconnect { m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); } + /// @brief Upgrade the current connection to a websocket connection. + SessionPtr upgradeToWebsocket(RequestMessage &&msg); + protected: boost::beast::tcp_stream m_stream; }; diff --git a/src/mtconnect/sink/rest_sink/websocket_session.hpp b/src/mtconnect/sink/rest_sink/websocket_session.hpp new file mode 100644 index 00000000..7a84bfbc --- /dev/null +++ b/src/mtconnect/sink/rest_sink/websocket_session.hpp @@ -0,0 +1,543 @@ +// +// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// 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 "mtconnect/config.hpp" +#include "mtconnect/configuration/config_options.hpp" +#include "mtconnect/utilities.hpp" +#include "session.hpp" + +namespace mtconnect::sink::rest_sink { + namespace beast = boost::beast; + + struct WebsocketRequest + { + WebsocketRequest(const std::string &id) : m_requestId(id) {} + std::string m_requestId; + std::optional m_streamBuffer; + Complete m_complete; + bool m_streaming {false}; + RequestPtr m_request; + }; + + /// @brief A websocket session that provides a pubsub interface using REST parameters + template + class WebsocketSession : public Session + { + protected: + struct Message + { + Message(const std::string &body, Complete &complete, const std::string &requestId) + : m_body(body), m_complete(complete), m_requestId(requestId) + {} + + std::string m_body; + Complete m_complete; + std::string m_requestId; + }; + + public: + using RequestMessage = boost::beast::http::request; + + WebsocketSession(RequestPtr &&request, RequestMessage &&msg, Dispatch dispatch, + ErrorFunction func) + : Session(dispatch, func), m_request(std::move(request)), m_msg(std::move(msg)) + {} + + /// @brief Session cannot be copied. + WebsocketSession(const WebsocketSession &) = delete; + ~WebsocketSession() = default; + + /// @brief get this as the `Derived` type + /// @return the subclass + Derived &derived() { return static_cast(*this); } + + void run() override + { + using namespace boost::beast; + + // Set suggested timeout settings for the websocket + derived().stream().set_option( + websocket::stream_base::timeout::suggested(beast::role_type::server)); + + // Set a decorator to change the Server of the handshake + derived().stream().set_option( + websocket::stream_base::decorator([](websocket::response_type &res) { + res.set(http::field::server, GetAgentVersion() + " MTConnectAgent"); + })); + + // Accept the websocket handshake + derived().stream().async_accept( + m_msg, boost::asio::bind_executor(derived().getExecutor(), + beast::bind_front_handler(&WebsocketSession::onAccept, + derived().shared_ptr()))); + } + + void close() override + { + NAMED_SCOPE("PlainWebsocketSession::close"); + if (!m_isOpen) + return; + + m_isOpen = false; + + auto wptr = weak_from_this(); + std::shared_ptr ptr; + if (!wptr.expired()) + { + ptr = wptr.lock(); + } + + m_request.reset(); + m_requests.clear(); + for (auto obs : m_observers) + { + auto optr = obs.lock(); + if (optr) + { + optr->cancel(); + } + } + closeStream(); + } + + void writeResponse(ResponsePtr &&response, Complete complete = nullptr) override + { + NAMED_SCOPE("WebsocketSession::writeResponse"); + if (!response->m_requestId) + { + boost::system::error_code ec; + return fail(status::bad_request, "Missing request Id", ec); + } + + writeChunk(response->m_body, complete, response->m_requestId); + } + + void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) override + { + NAMED_SCOPE("WebsocketSession::writeFailureResponse"); + writeChunk(response->m_body, complete, response->m_requestId); + } + + void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) override + { + if (requestId) + { + auto id = *(requestId); + auto it = m_requests.find(id); + if (it != m_requests.end()) + { + auto &req = it->second; + req.m_streaming = true; + + if (complete) + { + complete(); + } + } + else + { + LOG(error) << "Cannot find request for id: " << id; + } + } + else + { + LOG(error) << "No request id for websocket"; + } + } + + void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) override + { + NAMED_SCOPE("WebsocketSession::writeChunk"); + + if (!derived().stream().is_open()) + { + return; + } + + if (requestId) + { + LOG(trace) << "Waiting for mutex"; + std::lock_guard lock(m_mutex); + + if (m_busy || m_messageQueue.size() > 0) + { + m_messageQueue.emplace_back(chunk, complete, *requestId); + } + else + { + send(chunk, complete, *requestId); + } + } + else + { + LOG(error) << "No request id for websocket"; + } + } + + protected: + void onAccept(boost::beast::error_code ec) + { + if (ec) + { + fail(status::internal_server_error, "Error occurred in accpet", ec); + return; + } + + m_isOpen = true; + + derived().stream().async_read( + m_buffer, beast::bind_front_handler(&WebsocketSession::onRead, derived().shared_ptr())); + } + + void send(const std::string body, Complete complete, const std::string &requestId) + { + NAMED_SCOPE("WebsocketSession::send"); + + using namespace std::placeholders; + + auto it = m_requests.find(requestId); + if (it != m_requests.end()) + { + auto &req = it->second; + req.m_complete = std::move(complete); + req.m_streamBuffer.emplace(); + std::ostream str(&req.m_streamBuffer.value()); + + str << body; + + auto ref = derived().shared_ptr(); + + LOG(trace) << "writing chunk for ws: " << requestId; + + m_busy = true; + + derived().stream().text(derived().stream().got_text()); + derived().stream().async_write(req.m_streamBuffer->data(), + beast::bind_handler( + [ref, requestId](beast::error_code ec, std::size_t len) { + ref->sent(ec, len, requestId); + }, + _1, _2)); + } + else + { + LOG(error) << "Cannot find request for id: " << requestId; + } + } + + void sent(beast::error_code ec, std::size_t len, const std::string &id) + { + NAMED_SCOPE("WebsocketSession::sent"); + + if (ec) + { + return fail(status::bad_request, "Missing request Id", ec); + } + + { + LOG(trace) << "Waiting for mutex"; + std::lock_guard lock(m_mutex); + + LOG(trace) << "sent chunk for ws: " << id; + + auto it = m_requests.find(id); + if (it != m_requests.end()) + { + auto &req = it->second; + if (req.m_complete) + { + boost::asio::post(derived().stream().get_executor(), req.m_complete); + req.m_complete = nullptr; + } + + if (!req.m_streaming) + { + m_requests.erase(id); + } + + if (m_messageQueue.size() == 0) + { + m_busy = false; + } + } + else + { + LOG(error) << "WebsocketSession::sent: Cannot find request for id: " << id; + } + } + + { + LOG(trace) << "Waiting for mutex to send next"; + std::lock_guard lock(m_mutex); + + // Check for queued messages + if (m_messageQueue.size() > 0) + { + auto &msg = m_messageQueue.front(); + send(msg.m_body, msg.m_complete, msg.m_requestId); + m_messageQueue.pop_front(); + } + } + } + + void onRead(beast::error_code ec, std::size_t len) + { + NAMED_SCOPE("PlainWebsocketSession::onRead"); + + if (ec) + return fail(boost::beast::http::status::internal_server_error, "shutdown", ec); + + using namespace rapidjson; + using namespace std; + + if (len == 0) + { + LOG(trace) << "Empty message received"; + return; + } + + // Parse the buffer as a JSON request with parameters matching + // REST API + derived().stream().text(derived().stream().got_text()); + auto buffer = beast::buffers_to_string(m_buffer.data()); + m_buffer.consume(m_buffer.size()); + + Document doc; + doc.Parse(buffer.c_str(), len); + + if (doc.HasParseError()) + { + LOG(warning) << "Websocket Read Error(offset (" << doc.GetErrorOffset() + << "): " << GetParseError_En(doc.GetParseError()); + LOG(warning) << " " << buffer; + } + if (!doc.IsObject()) + { + LOG(warning) << "Websocket Read Error: JSON message does not have a top level object"; + LOG(warning) << " " << buffer; + } + else + { + // Extract the parameters from the json doc to map them to the REST + // protocol parameters + auto request = make_unique(*m_request); + + request->m_verb = beast::http::verb::get; + request->m_parameters.clear(); +#ifdef GetObject +#define __GOSave__ GetObject +#undef GetObject +#endif + + const auto &object = doc.GetObject(); +#ifdef __GOSave__ +#define GetObject __GOSave__ +#endif + + for (auto &it : object) + { + switch (it.value.GetType()) + { + case rapidjson::kNullType: + // Skip nulls + break; + case rapidjson::kFalseType: + request->m_parameters.emplace( + make_pair(string(it.name.GetString()), ParameterValue(false))); + break; + case rapidjson::kTrueType: + request->m_parameters.emplace( + make_pair(string(it.name.GetString()), ParameterValue(true))); + break; + case rapidjson::kObjectType: + break; + case rapidjson::kArrayType: + break; + case rapidjson::kStringType: + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(string(it.value.GetString())))); + + break; + case rapidjson::kNumberType: + if (it.value.IsInt()) + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(it.value.GetInt()))); + else if (it.value.IsUint()) + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(uint64_t(it.value.GetUint())))); + else if (it.value.IsInt64()) + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(uint64_t(it.value.GetInt64())))); + else if (it.value.IsUint64()) + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(it.value.GetUint64()))); + else if (it.value.IsDouble()) + request->m_parameters.emplace( + make_pair(it.name.GetString(), ParameterValue(double(it.value.GetDouble())))); + + break; + } + } + + if (request->m_parameters.count("request") > 0) + { + request->m_command = get(request->m_parameters["request"]); + request->m_parameters.erase("request"); + } + if (request->m_parameters.count("id") > 0) + { + auto &v = request->m_parameters["id"]; + string id = visit(overloaded {[](monostate m) { return ""s; }, + [](auto v) { return boost::lexical_cast(v); }}, + v); + request->m_requestId = id; + request->m_parameters.erase("id"); + } + + auto &id = *(request->m_requestId); + auto res = m_requests.emplace(id, id); + if (!res.second) + { + LOG(error) << "Duplicate request id: " << id; + boost::system::error_code ec; + fail(status::bad_request, "Duplicate request Id", ec); + } + else + { + res.first->second.m_request = std::move(request); + if (!m_dispatch(derived().shared_ptr(), res.first->second.m_request)) + { + ostringstream txt; + txt << "Failed to find handler for " << buffer; + LOG(error) << txt.str(); + boost::system::error_code ec; + fail(status::bad_request, "Duplicate request Id", ec); + } + } + } + + derived().stream().async_read( + m_buffer, beast::bind_front_handler(&WebsocketSession::onRead, derived().shared_ptr())); + } + + protected: + RequestPtr m_request; + RequestMessage m_msg; + beast::flat_buffer m_buffer; + std::map m_requests; + std::mutex m_mutex; + std::atomic_bool m_busy; + std::deque m_messageQueue; + bool m_isOpen {false}; + }; + + template + using WebsocketSessionPtr = std::shared_ptr>; + + class PlainWebsocketSession : public WebsocketSession + { + public: + using Stream = beast::websocket::stream; + + PlainWebsocketSession(beast::tcp_stream &&stream, RequestPtr &&request, RequestMessage &&msg, + Dispatch dispatch, ErrorFunction func) + : WebsocketSession(std::move(request), std::move(msg), dispatch, func), + m_stream(std::move(stream)) + { + beast::get_lowest_layer(m_stream).expires_never(); + } + ~PlainWebsocketSession() + { + if (m_isOpen) + close(); + } + + void closeStream() override + { + if (m_isOpen && m_stream.is_open()) + m_stream.close(beast::websocket::close_code::none); + } + + auto getExecutor() { return m_stream.get_executor(); } + + auto &stream() { return m_stream; } + + /// @brief Get a pointer cast as an Websocket Session + /// @return shared pointer to an Websocket session + std::shared_ptr shared_ptr() + { + return std::dynamic_pointer_cast(shared_from_this()); + } + + protected: + Stream m_stream; + }; + + class TlsWebsocketSession : public WebsocketSession + { + public: + using Stream = beast::websocket::stream>; + + TlsWebsocketSession(beast::ssl_stream &&stream, RequestPtr &&request, + RequestMessage &&msg, Dispatch dispatch, ErrorFunction func) + : WebsocketSession(std::move(request), std::move(msg), dispatch, func), + m_stream(std::move(stream)) + { + beast::get_lowest_layer(m_stream).expires_never(); + } + ~TlsWebsocketSession() + { + if (m_isOpen) + close(); + } + + auto &stream() { return m_stream; } + + auto getExecutor() { return m_stream.get_executor(); } + + void closeStream() override + { + if (m_isOpen && m_stream.is_open()) + m_stream.close(beast::websocket::close_code::none); + } + + /// @brief Get a pointer cast as an TLS Websocket Session + /// @return shared pointer to an TLS Websocket session + std::shared_ptr shared_ptr() + { + return std::dynamic_pointer_cast(shared_from_this()); + } + + protected: + Stream m_stream; + }; + +} // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/validation/observations.hpp b/src/mtconnect/validation/observations.hpp index 0eebdbed..c5a843ed 100644 --- a/src/mtconnect/validation/observations.hpp +++ b/src/mtconnect/validation/observations.hpp @@ -23,13 +23,13 @@ #include "../utilities.hpp" namespace mtconnect { - + /// @brief MTConnect validation containers namespace validation { - + /// @brief Observation validation containers namespace observations { - + /// @brief Validation type for observations using Validation = std::unordered_map>; diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt index 08a4e1ff..05427a32 100644 --- a/test_package/CMakeLists.txt +++ b/test_package/CMakeLists.txt @@ -245,12 +245,12 @@ add_agent_test(qname FALSE entity) add_agent_test(file_cache FALSE sink/rest_sink) add_agent_test(http_server FALSE sink/rest_sink TRUE) +add_agent_test(websockets FALSE sink/rest_sink TRUE) add_agent_test(tls_http_server FALSE sink/rest_sink TRUE) add_agent_test(routing FALSE sink/rest_sink) add_agent_test(mqtt_isolated FALSE mqtt_isolated TRUE) add_agent_test(mqtt_sink FALSE sink/mqtt_sink TRUE) -add_agent_test(mqtt_sink_2 FALSE sink/mqtt_sink_2 TRUE) add_agent_test(json_printer_asset TRUE json) add_agent_test(json_printer_error TRUE json) diff --git a/test_package/agent_test.cpp b/test_package/agent_test.cpp index f3cabd50..c859d1a5 100644 --- a/test_package/agent_test.cpp +++ b/test_package/agent_test.cpp @@ -3139,7 +3139,6 @@ TEST_F(AgentTest, should_not_set_validation_flag_in_header_when_validation_is_fa } } - TEST_F(AgentTest, should_initialize_observaton_to_initial_value_when_available) { m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.2", 4, true); @@ -3153,7 +3152,7 @@ TEST_F(AgentTest, should_initialize_observaton_to_initial_value_when_available) PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:PartCount", "UNAVAILABLE"); } - + m_agentTestHelper->m_adapter->processData("2024-01-22T20:00:00Z|avail|AVAILABLE"); { diff --git a/test_package/agent_test_helper.hpp b/test_package/agent_test_helper.hpp index 13c14fc9..16c7a5cf 100644 --- a/test_package/agent_test_helper.hpp +++ b/test_package/agent_test_helper.hpp @@ -29,7 +29,6 @@ #include "mtconnect/configuration/agent_config.hpp" #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/pipeline/pipeline.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt2_service.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" #include "mtconnect/sink/rest_sink/response.hpp" #include "mtconnect/sink/rest_sink/rest_service.hpp" @@ -78,13 +77,15 @@ namespace mtconnect { writeResponse(std::move(response), complete); } } - void beginStreaming(const std::string &mimeType, Complete complete) override + void beginStreaming(const std::string &mimeType, Complete complete, + std::optional requestId = std::nullopt) override { m_mimeType = mimeType; m_streaming = true; complete(); } - void writeChunk(const std::string &chunk, Complete complete) override + void writeChunk(const std::string &chunk, Complete complete, + std::optional requestId = std::nullopt) override { m_chunkBody = chunk; if (m_streaming) @@ -123,7 +124,6 @@ class AgentTestHelper ~AgentTestHelper() { m_mqttService.reset(); - m_mqtt2Service.reset(); m_restService.reset(); m_adapter.reset(); if (m_agent) @@ -172,18 +172,9 @@ class AgentTestHelper std::shared_ptr getMqttService() { using namespace mtconnect; - sink::SinkPtr sink = m_agent->findSink("MqttService"); - std::shared_ptr mqtt = - std::dynamic_pointer_cast(sink); - return mqtt; - } - - std::shared_ptr getMqtt2Service() - { - using namespace mtconnect; - sink::SinkPtr mqttSink = m_agent->findSink("Mqtt2Service"); - std::shared_ptr mqtt2 = - std::dynamic_pointer_cast(mqttSink); + sink::SinkPtr mqttSink = m_agent->findSink("MqttService"); + std::shared_ptr mqtt2 = + std::dynamic_pointer_cast(mqttSink); return mqtt2; } @@ -198,7 +189,6 @@ class AgentTestHelper sink::rest_sink::RestService::registerFactory(m_sinkFactory); sink::mqtt_sink::MqttService::registerFactory(m_sinkFactory); - sink::mqtt_sink::Mqtt2Service::registerFactory(m_sinkFactory); source::adapter::shdr::ShdrAdapter::registerFactory(m_sourceFactory); ConfigOptions options = ops; @@ -234,20 +224,10 @@ class AgentTestHelper { auto mqttContract = m_agent->makeSinkContract(); mqttContract->m_pipelineContext = m_context; - auto mqttsink = m_sinkFactory.make("MqttService", "MqttService", m_ioContext, - std::move(mqttContract), options, ptree {}); - m_mqttService = std::dynamic_pointer_cast(mqttsink); - m_agent->addSink(m_mqttService); - } - - if (HasOption(options, "Mqtt2Sink")) - { - auto mqttContract = m_agent->makeSinkContract(); - mqttContract->m_pipelineContext = m_context; - auto mqtt2sink = m_sinkFactory.make("Mqtt2Service", "Mqtt2Service", m_ioContext, + auto mqtt2sink = m_sinkFactory.make("MqttService", "MqttService", m_ioContext, std::move(mqttContract), options, ptree {}); - m_mqtt2Service = std::dynamic_pointer_cast(mqtt2sink); - m_agent->addSink(m_mqtt2Service); + m_mqttService = std::dynamic_pointer_cast(mqtt2sink); + m_agent->addSink(m_mqttService); } m_agent->initialize(m_context); @@ -323,7 +303,6 @@ class AgentTestHelper std::shared_ptr m_context; std::shared_ptr m_adapter; std::shared_ptr m_mqttService; - std::shared_ptr m_mqtt2Service; std::shared_ptr m_restService; std::shared_ptr m_loopback; diff --git a/test_package/mqtt_sink_2_test.cpp b/test_package/mqtt_sink_2_test.cpp deleted file mode 100644 index bdae5342..00000000 --- a/test_package/mqtt_sink_2_test.cpp +++ /dev/null @@ -1,426 +0,0 @@ -// -// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// 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. -// -// Ensure that gtest is the first header otherwise Windows raises an error -#include -// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) - -#include - -#include - -#include "agent_test_helper.hpp" -#include "json_helper.hpp" -#include "mtconnect/buffer/checkpoint.hpp" -#include "mtconnect/device_model/data_item/data_item.hpp" -#include "mtconnect/entity/entity.hpp" -#include "mtconnect/entity/json_parser.hpp" -#include "mtconnect/mqtt/mqtt_client_impl.hpp" -#include "mtconnect/mqtt/mqtt_server_impl.hpp" -#include "mtconnect/printer//json_printer.hpp" -#include "mtconnect/sink/mqtt_sink/mqtt2_service.hpp" -#include "test_utilities.hpp" - -using namespace std; -using namespace mtconnect; -using namespace mtconnect::device_model::data_item; -using namespace mtconnect::sink::mqtt_sink; -using namespace mtconnect::asset; -using namespace mtconnect::configuration; - -// main -int main(int argc, char *argv[]) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} - -using json = nlohmann::json; - -class MqttSink2Test : public testing::Test -{ -protected: - void SetUp() override - { - m_agentTestHelper = make_unique(); - m_jsonPrinter = std::make_unique(2, true); - } - - void TearDown() override - { - const auto agent = m_agentTestHelper->getAgent(); - if (agent) - { - m_agentTestHelper->getAgent()->stop(); - m_agentTestHelper->m_ioContext.run_for(100ms); - } - if (m_client) - { - m_client->stop(); - m_agentTestHelper->m_ioContext.run_for(500ms); - m_client.reset(); - } - if (m_server) - { - m_server->stop(); - m_agentTestHelper->m_ioContext.run_for(500ms); - m_server.reset(); - } - m_agentTestHelper.reset(); - m_jsonPrinter.reset(); - } - - void createAgent(std::string testFile = {}, ConfigOptions options = {}) - { - if (testFile == "") - testFile = "/samples/test_config.xml"; - - ConfigOptions opts(options); - MergeOptions(opts, {{"Mqtt2Sink", true}, - {configuration::MqttPort, m_port}, - {MqttCurrentInterval, 200ms}, - {MqttSampleInterval, 100ms}, - {configuration::MqttHost, "127.0.0.1"s}}); - m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 25, false, true, opts); - addAdapter(); - - m_agentTestHelper->getAgent()->start(); - } - - void createServer(const ConfigOptions &options) - { - using namespace mtconnect::configuration; - ConfigOptions opts(options); - MergeOptions(opts, {{ServerIp, "127.0.0.1"s}, - {MqttPort, 0}, - {MqttTls, false}, - {AutoAvailable, false}, - {RealTime, false}}); - - m_server = - make_shared(m_agentTestHelper->m_ioContext, opts); - } - - template - bool waitFor(const chrono::duration &time, function pred) - { - boost::asio::steady_timer timer(m_agentTestHelper->m_ioContext); - timer.expires_after(time); - bool timeout = false; - timer.async_wait([&timeout](boost::system::error_code ec) { - if (!ec) - { - timeout = true; - } - }); - - while (!timeout && !pred()) - { - m_agentTestHelper->m_ioContext.run_for(100ms); - } - timer.cancel(); - - return pred(); - } - - void startServer() - { - if (m_server) - { - bool start = m_server->start(); - if (start) - { - m_port = m_server->getPort(); - m_agentTestHelper->m_ioContext.run_for(500ms); - } - } - } - - void createClient(const ConfigOptions &options, unique_ptr &&handler) - { - ConfigOptions opts(options); - MergeOptions(opts, {{MqttHost, "127.0.0.1"s}, - {MqttPort, m_port}, - {MqttTls, false}, - {AutoAvailable, false}, - {RealTime, false}}); - m_client = make_shared(m_agentTestHelper->m_ioContext, - opts, std::move(handler)); - } - - bool startClient() - { - bool started = m_client && m_client->start(); - if (started) - { - return waitFor(1s, [this]() { return m_client->isConnected(); }); - } - return started; - } - - void addAdapter(ConfigOptions options = ConfigOptions {}) - { - m_agentTestHelper->addAdapter(options, "localhost", 0, - m_agentTestHelper->m_agent->getDefaultDevice()->getName()); - } - - std::unique_ptr m_jsonPrinter; - std::shared_ptr m_server; - std::shared_ptr m_client; - std::unique_ptr m_agentTestHelper; - uint16_t m_port {0}; -}; - -TEST_F(MqttSink2Test, mqtt_sink_flat_formatt_check) -{ - ConfigOptions options {{MqttMaxTopicDepth, 9}, {ProbeTopic, "Device/F/l/a/t/F/o/r/m/a/t"s}}; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent("", options); - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(10s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Probe/000", topic); - - ErrorList list; - auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); - EXPECT_EQ(0, list.size()); - auto dev = dynamic_pointer_cast(ptr); - EXPECT_TRUE(dev); - EXPECT_EQ("LinuxCNC", dev->getComponentName()); - EXPECT_EQ("000", *dev->getUuid()); - - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Probe/000"); - - createAgent(); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Sample) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotSample = false; - handler->m_receive = [&gotSample](std::shared_ptr client, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Sample/000", topic); - - auto jdoc = json::parse(payload); - auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); - EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - - gotSample = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Sample/000"); - - createAgent(); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_FALSE(gotSample); - - m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); - ASSERT_TRUE(waitFor(10s, [&gotSample]() { return gotSample; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Current) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotCurrent = false; - handler->m_receive = [&gotCurrent](std::shared_ptr client, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Current/000", topic); - - auto jdoc = json::parse(payload); - auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); - EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - - gotCurrent = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Current/000"); - - createAgent(); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_TRUE(gotCurrent); - - gotCurrent = false; - ASSERT_TRUE(waitFor(1s, [&gotCurrent]() { return gotCurrent; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_with_uuid_first) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/000/Probe", topic); - - ErrorList list; - auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); - EXPECT_EQ(0, list.size()); - auto dev = dynamic_pointer_cast(ptr); - EXPECT_TRUE(dev); - EXPECT_EQ("LinuxCNC", dev->getComponentName()); - EXPECT_EQ("000", *dev->getUuid()); - - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/000/Probe"); - - createAgent("", {{configuration::ProbeTopic, "MTConnect/[device]/Probe"s}}); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_Probe_no_device_in_format) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Probe/000", topic); - - ErrorList list; - auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); - EXPECT_EQ(0, list.size()); - auto dev = dynamic_pointer_cast(ptr); - EXPECT_TRUE(dev); - EXPECT_EQ("LinuxCNC", dev->getComponentName()); - EXPECT_EQ("000", *dev->getUuid()); - - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Probe/000"); - - createAgent("", {{configuration::ProbeTopic, "MTConnect/Probe"s}}); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} - -TEST_F(MqttSink2Test, mqtt_sink_should_publish_agent_device) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - DevicePtr ad; - string agent_topic; - - auto handler = make_unique(); - bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser, &agent_topic, &ad](std::shared_ptr client, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ(agent_topic, topic); - gotDevice = true; - }; - - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent(); - - ad = m_agentTestHelper->m_agent->getAgentDevice(); - agent_topic = "MTConnect/Probe/Agent_"s + *ad->getUuid(); - m_client->subscribe(agent_topic); - - auto service = m_agentTestHelper->getMqtt2Service(); - - ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - - ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); -} diff --git a/test_package/mqtt_sink_test.cpp b/test_package/mqtt_sink_test.cpp index 6675811f..e3c63af4 100644 --- a/test_package/mqtt_sink_test.cpp +++ b/test_package/mqtt_sink_test.cpp @@ -14,10 +14,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // - -/// @file -/// Test MQTT 1 Service - // Ensure that gtest is the first header otherwise Windows raises an error #include // Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) @@ -27,6 +23,7 @@ #include #include "agent_test_helper.hpp" +#include "json_helper.hpp" #include "mtconnect/buffer/checkpoint.hpp" #include "mtconnect/device_model/data_item/data_item.hpp" #include "mtconnect/entity/entity.hpp" @@ -35,6 +32,7 @@ #include "mtconnect/mqtt/mqtt_server_impl.hpp" #include "mtconnect/printer//json_printer.hpp" #include "mtconnect/sink/mqtt_sink/mqtt_service.hpp" +#include "test_utilities.hpp" using namespace std; using namespace mtconnect; @@ -64,6 +62,7 @@ class MqttSinkTest : public testing::Test void TearDown() override { const auto agent = m_agentTestHelper->getAgent(); + m_agentTestHelper->m_ioContext.run_for(500ms); if (agent) { m_agentTestHelper->getAgent()->stop(); @@ -72,7 +71,7 @@ class MqttSinkTest : public testing::Test if (m_client) { m_client->stop(); - m_agentTestHelper->m_ioContext.run_for(100ms); + m_agentTestHelper->m_ioContext.run_for(500ms); m_client.reset(); } if (m_server) @@ -93,8 +92,10 @@ class MqttSinkTest : public testing::Test ConfigOptions opts(options); MergeOptions(opts, {{"MqttSink", true}, {configuration::MqttPort, m_port}, + {MqttCurrentInterval, 200ms}, + {MqttSampleInterval, 100ms}, {configuration::MqttHost, "127.0.0.1"s}}); - m_agentTestHelper->createAgent(testFile, 8, 4, "2.0", 25, false, true, opts); + m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 25, false, true, opts); addAdapter(); m_agentTestHelper->getAgent()->start(); @@ -118,7 +119,7 @@ class MqttSinkTest : public testing::Test bool waitFor(const chrono::duration &time, function pred) { boost::asio::steady_timer timer(m_agentTestHelper->m_ioContext); - timer.expires_from_now(time); + timer.expires_after(time); bool timeout = false; timer.async_wait([&timeout](boost::system::error_code ec) { if (!ec) @@ -166,7 +167,7 @@ class MqttSinkTest : public testing::Test bool started = m_client && m_client->start(); if (started) { - return waitFor(5s, [this]() { return m_client->isConnected(); }); + return waitFor(1s, [this]() { return m_client->isConnected(); }); } return started; } @@ -180,36 +181,13 @@ class MqttSinkTest : public testing::Test std::unique_ptr m_jsonPrinter; std::shared_ptr m_server; std::shared_ptr m_client; - std::shared_ptr m_service; std::unique_ptr m_agentTestHelper; uint16_t m_port {0}; }; -TEST_F(MqttSinkTest, mqtt_sink_should_be_loaded_by_agent) -{ - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - - ASSERT_TRUE(service); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker) +TEST_F(MqttSinkTest, mqtt_sink_flat_formatt_check) { - ConfigOptions options; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_with_UserNameandPassword) -{ - ConfigOptions options {{MqttUserName, "MQTT-SINK"s}, {MqttPassword, "mtconnect"s}}; + ConfigOptions options {{MqttMaxTopicDepth, 9}, {ProbeTopic, "Device/F/l/a/t/F/o/r/m/a/t"s}}; createServer(options); startServer(); @@ -218,24 +196,10 @@ TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_with_UserNameandPassword createAgent("", options); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_connect_to_broker_without_UserNameandPassword) -{ - ConfigOptions options; - createServer(options); - startServer(); - - ASSERT_NE(0, m_port); - - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + ASSERT_TRUE(waitFor(10s, [&service]() { return service->isConnected(); })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Probe) { ConfigOptions options; createServer(options); @@ -248,7 +212,7 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) bool gotDevice = false; handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Device/000", topic); + EXPECT_EQ("MTConnect/Probe/000", topic); ErrorList list; auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); @@ -263,18 +227,18 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_device) createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); - m_client->subscribe("MTConnect/Device/000"); + m_client->subscribe("MTConnect/Probe/000"); createAgent(); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_TRUE(waitFor(5s, [&gotDevice]() { return gotDevice; })); + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Streams) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Sample) { ConfigOptions options; createServer(options); @@ -284,64 +248,34 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_Streams) entity::JsonParser parser; auto handler = make_unique(); - bool foundLineDataItem = false; - handler->m_receive = [&foundLineDataItem](std::shared_ptr client, - const std::string &topic, const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]", topic); + bool gotSample = false; + handler->m_receive = [&gotSample](std::shared_ptr client, const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Sample/000", topic); auto jdoc = json::parse(payload); - string value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ("204", value); - foundLineDataItem = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); + auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); + EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - - m_client->subscribe("MTConnect/Observation/000/Controller[Controller]/Path/Events/Line[line]"); - m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); - - ASSERT_TRUE(waitFor(5s, [&foundLineDataItem]() { return foundLineDataItem; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Asset) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - - entity::JsonParser parser; - - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Asset/0001", topic); - auto jdoc = json::parse(payload); - string id = jdoc.at("/Part/assetId"_json_pointer).get(); - EXPECT_EQ("0001", id); - gotControllerDataItem = true; + gotSample = true; }; + createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/Sample/000"); createAgent(); + auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Asset/0001"); - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|@1|Part|TEST 1"); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + ASSERT_FALSE(gotSample); - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); + m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|line|204"); + ASSERT_TRUE(waitFor(10s, [&gotSample]() { return gotSample; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_RotaryMode) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Current) { ConfigOptions options; createServer(options); @@ -351,135 +285,34 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_RotaryMode) entity::JsonParser parser; auto handler = make_unique(); - bool gotRotaryMode = false; - handler->m_receive = [&gotRotaryMode](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]", - topic); + bool gotCurrent = false; + handler->m_receive = [&gotCurrent](std::shared_ptr client, const std::string &topic, + const std::string &payload) { + EXPECT_EQ("MTConnect/Current/000", topic); + auto jdoc = json::parse(payload); + auto streams = jdoc.at("/MTConnectStreams/Streams/0/DeviceStream"_json_pointer); + EXPECT_EQ(string("LinuxCNC"), streams.at("/name"_json_pointer).get()); - double v = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(5000.0, v); - gotRotaryMode = true; + gotCurrent = true; }; createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/Current/000"); createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Samples/SpindleSpeed.Actual[Sspeed]"); - - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|block|G01X00|Sspeed|5000|line|204"); - - ASSERT_TRUE(waitFor(5s, [&gotRotaryMode]() { return gotRotaryMode; })); -} - -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Dataset) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]", - topic); - auto jdoc = json::parse(payload); - auto id = jdoc.at("/value/a"_json_pointer).get(); - EXPECT_EQ(1, id); - gotControllerDataItem = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent("/samples/data_set.xml"); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/VariableDataSet[vars]"); - - m_agentTestHelper->m_adapter->processData("TIME|vars|a=1 b=2 c=3"); - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); -} -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Table) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotControllerDataItem = false; - handler->m_receive = [&gotControllerDataItem](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/WorkOffsetTable[wpo]", - topic); - auto jdoc = json::parse(payload); - - auto jValue = jdoc.at("/value"_json_pointer); - int count = 0; - if (jValue.is_object()) - { - for (auto &[key, value] : jValue.items()) - { - if (key == "G53.1" || key == "G53.2" || key == "G53.3") - { - for (auto &[subKey, subValue] : value.items()) - { - if (key == "G53.1" && ((subKey == "X" && subValue.get() == 1) || - (subKey == "Y" && subValue.get() == 2) || - (subKey == "Z" && subValue.get() == 3))) - { - count++; - } - else if (key == "G53.2" && ((subKey == "X" && subValue.get() == 4) || - (subKey == "Y" && subValue.get() == 5) || - (subKey == "Z" && subValue.get() == 6))) - { - count++; - } - else if (key == "G53.3" && ((subKey == "X" && subValue.get() == 7.0) || - (subKey == "Y" && subValue.get() == 8.0) || - (subKey == "Z" && subValue.get() == 9.0) || - (subKey == "U" && subValue.get() == 10.0))) - { - count++; - } - } - } - } - EXPECT_EQ(10, count); - gotControllerDataItem = true; - } - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent("/samples/data_set.xml"); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Controller[Controller]/Path[path]/Events/" - "WorkOffsetTable[wpo]"); - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|wpo|G53.1={X=1.0 Y=2.0 Z=3.0} G53.2={X=4.0 Y=5.0 Z=6.0}" - "G53.3={X=7.0 Y=8.0 Z=9 U=10.0}"); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + ASSERT_TRUE(gotCurrent); - ASSERT_TRUE(waitFor(5s, [&gotControllerDataItem]() { return gotControllerDataItem; })); + gotCurrent = false; + ASSERT_TRUE(waitFor(1s, [&gotCurrent]() { return gotCurrent; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_Temperature) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Probe_with_uuid_first) { ConfigOptions options; createServer(options); @@ -489,64 +322,36 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_Temperature) entity::JsonParser parser; auto handler = make_unique(); - bool gotTemperature = false; - handler->m_receive = [&gotTemperature](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" - "Temperature[z_motor_temp]", - topic); - auto jdoc = json::parse(payload); + bool gotDevice = false; + handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, + const std::string &topic, const std::string &payload) { + EXPECT_EQ("MTConnect/000/Probe", topic); + + ErrorList list; + auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); + EXPECT_EQ(0, list.size()); + auto dev = dynamic_pointer_cast(ptr); + EXPECT_TRUE(dev); + EXPECT_EQ("LinuxCNC", dev->getComponentName()); + EXPECT_EQ("000", *dev->getUuid()); - auto value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(81.0, value); - gotTemperature = true; + gotDevice = true; }; createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/000/Probe"); - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Linear[Z]/Motor[motor_name]/Samples/" - "Temperature[z_motor_temp]"); - - m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|z_motor_temp|81"); - - ASSERT_TRUE(waitFor(5s, [&gotTemperature]() { return gotTemperature; })); -} + createAgent("", {{configuration::ProbeTopic, "MTConnect/[device]/Probe"s}}); -TEST_F(MqttSinkTest, mqtt_sink_should_publish_LinearLoad) -{ - ConfigOptions options; - createServer(options); - startServer(); - ASSERT_NE(0, m_port); - entity::JsonParser parser; - auto handler = make_unique(); - bool gotLinearLoad = false; - handler->m_receive = [&gotLinearLoad](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]", topic); - auto jdoc = json::parse(payload); - auto value = jdoc.at("/value"_json_pointer).get(); - EXPECT_EQ(50.0, value); - gotLinearLoad = true; - }; - createClient(options, std::move(handler)); - ASSERT_TRUE(startClient()); - createAgent(); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/Load[Xload]"); - m_agentTestHelper->m_adapter->processData("2018-04-27T05:00:26.555666|Xload|50"); - ASSERT_TRUE(waitFor(5s, [&gotLinearLoad]() { return gotLinearLoad; })); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -TEST_F(MqttSinkTest, mqtt_sink_should_publish_DynamicCalibration) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_Probe_no_device_in_format) { ConfigOptions options; createServer(options); @@ -556,75 +361,67 @@ TEST_F(MqttSinkTest, mqtt_sink_should_publish_DynamicCalibration) entity::JsonParser parser; auto handler = make_unique(); - bool gotCalibration = false; - handler->m_receive = [this, &gotCalibration](std::shared_ptr, - const std::string &topic, - const std::string &payload) { - EXPECT_EQ( - "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]", - topic); - auto jdoc = json::parse(payload); + bool gotDevice = false; + handler->m_receive = [&gotDevice, &parser](std::shared_ptr client, + const std::string &topic, const std::string &payload) { + EXPECT_EQ("MTConnect/Probe/000", topic); - auto value = jdoc.at("/value"_json_pointer); - ASSERT_TRUE(value.is_array()); - EXPECT_EQ(25, value.size()); - gotCalibration = true; + ErrorList list; + auto ptr = parser.parse(device_model::Device::getRoot(), payload, "2.0", list); + EXPECT_EQ(0, list.size()); + auto dev = dynamic_pointer_cast(ptr); + EXPECT_TRUE(dev); + EXPECT_EQ("LinuxCNC", dev->getComponentName()); + EXPECT_EQ("000", *dev->getUuid()); + + gotDevice = true; }; createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); + m_client->subscribe("MTConnect/Probe/000"); - createAgent(); - auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); + createAgent("", {{configuration::ProbeTopic, "MTConnect/Probe"s}}); - m_client->subscribe( - "MTConnect/Observation/000/Axes[Axes]/Linear[X]/Samples/PositionTimeSeries.Actual[Xts]"); + auto service = m_agentTestHelper->getMqttService(); - m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|Xts|25|| 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 5118 " - "5119 5119 5118 " - "5118 5117 5117 5119 5119 5118 5118 5118 5118 5118"); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - ASSERT_TRUE(waitFor(5s, [&gotCalibration]() { return gotCalibration; })); + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } -/// @test check if the condition includes the state as the key -TEST_F(MqttSinkTest, mqtt_should_publish_conditions_with_the_state_as_the_key) +TEST_F(MqttSinkTest, mqtt_sink_should_publish_agent_device) { ConfigOptions options; createServer(options); startServer(); ASSERT_NE(0, m_port); + entity::JsonParser parser; - auto handler = make_unique(); - bool gotCondition = false; - handler->m_receive = [&gotCondition](std::shared_ptr, const std::string &topic, - const std::string &payload) { - EXPECT_EQ("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature", topic); - auto jdoc = json::parse(payload); - EXPECT_EQ("Temperature is too high", jdoc.at("/Fault/value"_json_pointer).get()); - EXPECT_EQ("X111", jdoc.at("/Fault/nativeCode"_json_pointer).get()); - EXPECT_EQ("BAD", jdoc.at("/Fault/nativeSeverity"_json_pointer).get()); - EXPECT_EQ("HIGH", jdoc.at("/Fault/qualifier"_json_pointer).get()); - EXPECT_EQ("TEMPERATURE", jdoc.at("/Fault/type"_json_pointer).get()); + DevicePtr ad; + string agent_topic; - gotCondition = true; + auto handler = make_unique(); + bool gotDevice = false; + handler->m_receive = [&gotDevice, &parser, &agent_topic, &ad](std::shared_ptr client, + const std::string &topic, + const std::string &payload) { + EXPECT_EQ(agent_topic, topic); + gotDevice = true; }; + createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); createAgent(); - auto di = m_agentTestHelper->m_agent->getDataItemById("ctmp"); - ASSERT_TRUE(di); - ASSERT_EQ("000/Axes[Axes]/Rotary[C]/Condition/Temperature", di->getTopic()); + ad = m_agentTestHelper->m_agent->getAgentDevice(); + agent_topic = "MTConnect/Probe/Agent_"s + *ad->getUuid(); + m_client->subscribe(agent_topic); auto service = m_agentTestHelper->getMqttService(); - ASSERT_TRUE(waitFor(5s, [&service]() { return service->isConnected(); })); - m_client->subscribe("MTConnect/Observation/000/Axes[Axes]/Rotary[C]/Condition/Temperature"); - m_agentTestHelper->m_adapter->processData( - "2018-04-27T05:00:26.555666|ctmp|fault|X111|BAD|HIGH|Temperature is too high"); - ASSERT_TRUE(waitFor(5s, [&gotCondition]() { return gotCondition; })); + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); + + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } diff --git a/test_package/resources/samples/test_config.xml b/test_package/resources/samples/test_config.xml index b42a291c..cda9baf0 100644 --- a/test_package/resources/samples/test_config.xml +++ b/test_package/resources/samples/test_config.xml @@ -1,6 +1,6 @@ -
+
Linux CNC Device diff --git a/test_package/websockets_test.cpp b/test_package/websockets_test.cpp new file mode 100644 index 00000000..0ca15442 --- /dev/null +++ b/test_package/websockets_test.cpp @@ -0,0 +1,246 @@ +// +// Copyright Copyright 2009-2024, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// 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. +// + +// Ensure that gtest is the first header otherwise Windows raises an error +#include +// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "mtconnect/logging.hpp" +#include "mtconnect/sink/rest_sink/server.hpp" + +using namespace std; +using namespace mtconnect; +using namespace mtconnect::sink::rest_sink; + +namespace asio = boost::asio; +namespace beast = boost::beast; +namespace http = boost::beast::http; +using tcp = boost::asio::ip::tcp; +namespace websocket = beast::websocket; + +// main +int main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +class Client +{ +public: + Client(asio::io_context& ioc) : m_context(ioc), m_stream(ioc) {} + + ~Client() { close(); } + + void fail(beast::error_code ec, char const* what) + { + LOG(error) << what << ": " << ec.message() << "\n"; + m_done = true; + m_ec = ec; + } + + void connect(unsigned short port, asio::yield_context yield) + { + beast::error_code ec; + + // These objects perform our I/O + tcp::endpoint server(asio::ip::address_v4::from_string("127.0.0.1"), port); + + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(m_stream).async_connect(server, yield[ec]); + + if (ec) + { + return fail(ec, "connect"); + } + + m_stream.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + + m_stream.set_option(websocket::stream_base::decorator([](websocket::request_type& req) { + req.set(http::field::user_agent, + std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client"); + })); + + string host = "127.0.0.1:" + std::to_string(port); + m_stream.async_handshake(host, "/", yield[ec]); + + if (ec) + { + return fail(ec, "connect"); + } + + m_connected = true; + + m_stream.async_read(m_buffer, beast::bind_front_handler(&Client::onRead, this)); + } + + void onRead(beast::error_code ec, std::size_t bytes_transferred) + { + m_result = beast::buffers_to_string(m_buffer.data()); + m_buffer.consume(m_buffer.size()); + + m_done = true; + } + + void request(const string& payload, asio::yield_context yield) + { + cout << "spawnRequest: done: false" << endl; + m_done = false; + beast::error_code ec; + + m_stream.async_write(asio::buffer(payload), yield[ec]); + + waitFor(2s, [this]() { return m_done; }); + } + + template + bool waitFor(const chrono::duration& time, function pred) + { + boost::asio::steady_timer timer(m_context); + timer.expires_from_now(time); + bool timeout = false; + timer.async_wait([&timeout](boost::system::error_code ec) { + if (!ec) + { + timeout = true; + } + }); + + while (!timeout && !pred()) + { + m_context.run_for(500ms); + } + timer.cancel(); + + return pred(); + } + + void close() + { + beast::error_code ec; + + // Gracefully close the socket + m_stream.next_layer().shutdown(tcp::socket::shutdown_both, ec); + } + + bool m_connected {false}; + int m_status; + std::string m_result; + asio::io_context& m_context; + bool m_done {false}; + websocket::stream m_stream; + beast::flat_buffer m_buffer; + boost::beast::error_code m_ec; + beast::flat_buffer m_b; + int m_count {0}; +}; + +class WebsocketsTest : public testing::Test +{ +protected: + void SetUp() override + { + using namespace mtconnect::configuration; + m_server = make_unique(m_context, ConfigOptions {{Port, 0}, {ServerIp, "127.0.0.1"s}}); + } + + void createServer(const ConfigOptions& options) + { + using namespace mtconnect::configuration; + ConfigOptions opts {{Port, 0}, {ServerIp, "127.0.0.1"s}}; + opts.merge(ConfigOptions(options)); + m_server = make_unique(m_context, opts); + } + + void start() + { + m_server->start(); + while (!m_server->isListening()) + m_context.run_one(); + m_client = make_unique(m_context); + } + + void startClient() + { + m_client->m_connected = false; + asio::spawn(m_context, + std::bind(&Client::connect, m_client.get(), + static_cast(m_server->getPort()), std::placeholders::_1)); + + m_client->waitFor(1s, [this]() { return m_client->m_connected; }); + } + + void TearDown() override + { + m_server.reset(); + m_client.reset(); + } + + asio::io_context m_context; + unique_ptr m_server; + unique_ptr m_client; +}; + +TEST_F(WebsocketsTest, should_connect_to_server) +{ + start(); + startClient(); + + ASSERT_TRUE(m_client->m_connected); +} + +TEST_F(WebsocketsTest, should_make_simple_request) +{ + weak_ptr savedSession; + + auto probe = [&](SessionPtr session, RequestPtr request) -> bool { + savedSession = session; + ResponsePtr resp = make_unique(status::ok); + resp->m_body = "All Devices for "s + *request->m_requestId; + resp->m_requestId = request->m_requestId; + session->writeResponse(std::move(resp), []() { cout << "Written" << endl; }); + return true; + }; + + m_server->addRouting({boost::beast::http::verb::get, "/probe", probe}).command("probe"); + m_server->addCommands(); + + start(); + startClient(); + + asio::spawn(m_context, std::bind(&Client::request, m_client.get(), + "{\"id\":\"1\",\"request\":\"probe\"}"s, std::placeholders::_1)); + + m_client->waitFor(2s, [this]() { return m_client->m_done; }); + + ASSERT_TRUE(m_client->m_done); + ASSERT_EQ("All Devices for 1", m_client->m_result); +}