diff --git a/README.md b/README.md index a6ba8432..c01f44a0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ protocol and data collection framework that will work as a standalone server. Once built, you only need to specify the XML description of the devices and the location of the adapter. +**NOTE: This version cannot currently be built on Windows XP since there is currently no support for the XP toolchain and C++ 17.** + 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.1.0 Added MQTT Sink, Agent Restart and new JSON format (version 2) @@ -769,7 +771,6 @@ The following parameters must be present to enable https requests. If there is n not found the default device if only one device is specified in the devices file. - * `Host` - The host the adapter is located on. *Default*: localhost @@ -849,12 +850,16 @@ The following parameters must be present to enable https requests. If there is n *Default*: Top Level Setting - * `ShdrVersion` - Specifies the SHDR protocol version used by the adapter. When greater than one (1), allows multiple complex observations, like `Condition` and `Message` on the same line. If it equials one (1), then any observation requiring more than a key/value pair need to be on separate lines. Applies to only this adapter. + * `ShdrVersion` - Specifies the SHDR protocol version used by the adapter. When greater than one + (1), allows multiple complex observations, like `Condition` and `Message` on the same line. + If it equials one (1), then any observation requiring more than a key/value pair need to be on + separate lines. Applies to only this adapter. *Default*: 1 - * `SuppressIPAddress` - Suppress the Adapter IP Address and port when creating the Agent Device ids and names. - *Default*: false + * `SuppressIPAddress` - Suppress the Adapter IP Address and port when creating the Agent Device ids and names. + + *Default*: false ### Agent Adapter Configuration diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index 99e1224e..d77c8093 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -406,3 +406,8 @@ endif() target_compile_features(agent_lib PUBLIC ${CXX_COMPILE_FEATURES}) target_clangformat_setup(agent_lib) target_clangtidy_setup(agent_lib) + +include(../cmake/document.cmake) +if (DOXYGEN_FOUND) + doxygen_add_docs(docs ../README.md ${AGENT_SOURCES}) +endif() diff --git a/appveyor.yml b/appveyor.yml index cf05ebc3..be3e08b2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -50,7 +50,7 @@ before_build: pip install conan conan export conan/mqtt_cpp conan export conan/mruby - conan install . -if build --build=missing -pr %CONAN_PROFILE% -o run_tests=False -o shared=%SHARED% + conan install . -if build --build=missing -pr %CONAN_PROFILE% -o run_tests=False -o shared=%SHARED% -o with_docs=False - sh: |- export PATH=$HOME/venv3.10/bin:$PATH if [[ $APPVEYOR_BUILD_WORKER_IMAGE = "Ubuntu2004" ]] @@ -68,7 +68,7 @@ before_build: pip install conan conan export conan/mqtt_cpp conan export conan/mruby - conan install . -if build --build=missing -pr $CONAN_PROFILE -o run_tests=False -o shared=$SHARED + conan install . -if build --build=missing -pr $CONAN_PROFILE -o run_tests=False -o shared=$SHARED -o with_docs=False build: verbosity: minimal diff --git a/cmake/document.cmake b/cmake/document.cmake new file mode 100644 index 00000000..db0b8e62 --- /dev/null +++ b/cmake/document.cmake @@ -0,0 +1,20 @@ +find_package(Doxygen OPTIONAL_COMPONENTS dot) +if (DOXYGEN_FOUND) + set(DOXYGEN_PROJECT_NAME "MTConnect C++ Agent") + set(DOXYGEN_PROJECT_BRIEF "MTConnect Reference C++ Agent") + set(DOXYGEN_PROJECT_LOGO "${PROJECT_SOURCE_DIR}/styles/favicon.ico") + set(DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Documentation") + set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${PROJECT_SOURCE_DIR}/README.md") + set(DOXYGEN_GENERATE_LATEX NO) + set(DOXYGEN_DOCSET_BUNDLE_ID org.mtconnect.agent) + set(DOXYGEN_DOCSET_PUBLISHER_ID org.amtonline) + set(DOXYGEN_DOCSET_PUBLISHER_NAME "Association for Manufacturing Technology") + set(DOXYGEN_TAB_SIZE 2) + set(DOXYGEN_BUILTIN_STL_SUPPORT YES) + set(DOXYGEN_UML_LOOK NO) + set(DOXYGEN_DOT_IMAGE_FORMAT svg) + set(DOXYGEN_INTERACTIVE_SVG YES) + set(DOXYGEN_INCLUDE_PATH "${CONAN_INCLUDE_DIRS}") + set(DOXYGEN_HTML_COLORSTYLE "AUTO_DARK") + set(DOXYGEN_MAX_DOT_GRAPH_DEPTH 3) +endif() diff --git a/conanfile.py b/conanfile.py index 7ee2f3ac..cd9d1132 100644 --- a/conanfile.py +++ b/conanfile.py @@ -5,6 +5,7 @@ import re import itertools as it import glob +import subprocess from conan.tools.microsoft import is_msvc, is_msvc_static_runtime class MTConnectAgentConan(ConanFile): @@ -16,7 +17,8 @@ class MTConnectAgentConan(ConanFile): settings = "os", "compiler", "arch", "build_type", "arch_build" options = { "run_tests": [True, False], "build_tests": [True, False], "without_ipv6": [True, False], "with_ruby": [True, False], - "development" : [True, False], "shared": [True, False], "winver": "ANY" } + "development" : [True, False], "shared": [True, False], "winver": "ANY", + "with_docs" : [True, False] } description = "MTConnect reference C++ agent copyright Association for Manufacturing Technology" requires = ["boost/1.79.0@#3249d9bd2b863a9489767bf9c8a05b4b", @@ -25,7 +27,7 @@ class MTConnectAgentConan(ConanFile): "nlohmann_json/3.9.1@#a41bc0deaf7f40e7b97e548359ccf14d", "openssl/3.0.5@#40f4488f02b36c1193b68f585131e8ef", "mqtt_cpp/13.1.0"] - + build_policy = "missing" default_options = { "run_tests": True, @@ -35,6 +37,7 @@ class MTConnectAgentConan(ConanFile): "development": False, "shared": False, "winver": "0x600", + "with_docs": True, "boost:shared": False, "boost:without_python": True, @@ -109,6 +112,12 @@ def configure(self): self.options["gtest"].shared = True self.options["openssl"].shared = True + def build_requirements(self): + if self.options.with_docs: + res = subprocess.run(["doxygen --version"], shell=True, text=True, capture_output=True) + if (res.returncode != 0 or not res.stdout.startswith('1.9')): + self.tool_requires("doxygen/1.9.4@#19fe2ac34109f3119190869a4d0ffbcb") + def requirements(self): if not self.windows_xp: self.requires("gtest/1.10.0") diff --git a/src/mtconnect/agent.cpp b/src/mtconnect/agent.cpp index 88fa39cc..1c108070 100644 --- a/src/mtconnect/agent.cpp +++ b/src/mtconnect/agent.cpp @@ -182,13 +182,13 @@ namespace mtconnect { void Agent::start() { NAMED_SCOPE("Agent::start"); - + if (m_started) { LOG(warning) << "Agent already started."; return; } - + try { m_beforeStartHooks.exec(*this); @@ -213,20 +213,20 @@ namespace mtconnect { LOG(fatal) << "Cannot start server: " << e.what(); std::exit(1); } - + m_started = true; } void Agent::stop() { NAMED_SCOPE("Agent::stop"); - + if (!m_started) { LOG(warning) << "Agent already stopped."; return; } - + m_beforeStopHooks.exec(*this); // Stop all adapter threads... @@ -248,7 +248,7 @@ namespace mtconnect { } LOG(info) << "Shutting down completed"; - + m_started = false; } @@ -272,7 +272,7 @@ namespace mtconnect { if (uuid) device = findDeviceByUUIDorName(*uuid); else - device = defaultDevice(); + device = getDefaultDevice(); if (device && device->getAssetChanged() && device->getAssetRemoved()) { @@ -898,7 +898,7 @@ namespace mtconnect { DevicePtr Agent::getDeviceByName(const std::string &name) const { if (name.empty()) - return defaultDevice(); + return getDefaultDevice(); auto &idx = m_deviceIndex.get(); auto devPos = idx.find(name); @@ -911,7 +911,7 @@ namespace mtconnect { DevicePtr Agent::getDeviceByName(const std::string &name) { if (name.empty()) - return defaultDevice(); + return getDefaultDevice(); auto &idx = m_deviceIndex.get(); auto devPos = idx.find(name); @@ -924,7 +924,7 @@ namespace mtconnect { DevicePtr Agent::findDeviceByUUIDorName(const std::string &idOrName) const { if (idOrName.empty()) - return defaultDevice(); + return getDefaultDevice(); DevicePtr res; if (auto d = m_deviceIndex.get().find(idOrName); d != m_deviceIndex.get().end()) diff --git a/src/mtconnect/agent.hpp b/src/mtconnect/agent.hpp index 08c70303..60966deb 100644 --- a/src/mtconnect/agent.hpp +++ b/src/mtconnect/agent.hpp @@ -47,7 +47,7 @@ #include "mtconnect/parser/xml_parser.hpp" #include "mtconnect/pipeline/pipeline.hpp" #include "mtconnect/pipeline/pipeline_contract.hpp" -#include "mtconnect/printer//printer.hpp" +#include "mtconnect/printer/printer.hpp" #include "mtconnect/sink/rest_sink/rest_service.hpp" #include "mtconnect/sink/rest_sink/server.hpp" #include "mtconnect/sink/sink.hpp" @@ -76,49 +76,99 @@ namespace mtconnect { using AssetChangeList = std::vector>; + /// Agent Class controls message flow and owns all sources and sinks. class AGENT_LIB_API Agent { public: - using Hook = std::function; - using HookList = std::list; - - // Load agent with the xml configuration + /// @brief Agent constructor + /// @param[in] context boost asio io context + /// @param[in] deviceXmlPath Device.xml configuration file path + /// @param[in] options Configuration Options + /// - SchemaVersion + /// - CheckpointFrequency + /// - Pretty + /// - VersionDeviceXmlUpdates + /// - JsonVersion + /// - DisableAgentDevice Agent(boost::asio::io_context &context, const std::string &deviceXmlPath, const ConfigOptions &options); - // Virtual destructor + /// Destructor for the Agent. + /// > Note: Does not stop the agent. ~Agent(); - // Hooks + /// @brief Hook callback type + using Hook = std::function; + + /// @brief Functions to run before the agent begins the initialization process. + /// @return configuration::HookManager& auto &beforeInitializeHooks() { return m_beforeInitializeHooks; } + + /// @brief Function that run after all agent initialization is complete + /// @return configuration::HookManager& auto &afterInitializeHooks() { return m_afterInitializeHooks; } + + /// @brief Hooks to run when before the agent starts all the soures and sinks + /// @return configuration::HookManager& auto &beforeStartHooks() { return m_beforeStartHooks; } + + /// @brief Hooks before the agent stops all the sources and sinks + /// @return configuration::HookManager& auto &beforeStopHooks() { return m_beforeStopHooks; } - // Initialize models and pipeline + /// @brief the agent given a pipeline context + /// @param context: the pipeline context shared between all pipelines void initialize(pipeline::PipelineContextPtr context); + + /// @brief initial UNAVAILABLE observations for all data items + /// unless they have constant values. void initialDataItemObservations(); // Start and stop + /// @brief Starts all the sources and sinks void start(); + /// @brief Stops all the sources and syncs. void stop(); + + /// @brief Get the boost asio io context + /// @return boost::asio::io_context auto &getContext() { return m_context; } - // Pipeline Contract + /// @brief Create a contract for pipelines to access agent information + /// @return A contract between the pipeline and this agent std::unique_ptr makePipelineContract(); + /// @brief Gets the pipe context shared by all pipelines + /// @return A shared pipeline context auto getPipelineContext() { return m_pipelineContext; } - // Sink Contract + /// @brief Makes a unique sink contract + /// @return A contract between the sink and the agent sink::SinkContractPtr makeSinkContract(); + /// @brief Get a reference to the XML parser + /// @return The XML parser const auto &getXmlParser() const { return m_xmlParser; } - + /// @brief Get a reference to the circular buffer. Used by sinks to + /// get latest and historical data. + /// @return A reference to the circular buffer auto &getCircularBuffer() { return m_circularBuffer; } - - // Add an adapter to the agent - void addSource(source::SourcePtr adapter, bool start = false); + /// @brief Get a const reference to the circular buffer. Used by sinks to + /// get latest and historical data. + /// @return A const reference to the circular buffer + const auto &getCircularBuffer() const { return m_circularBuffer; } + + /// @brief Adds an adapter to the agent + /// @param[in] source: shared pointer to the source being added + /// @param[in] start: starts the source if start is true, otherwise delayed start + void addSource(source::SourcePtr source, bool start = false); + /// @brief Adds a sink to the agent + /// @param[in] sink shared pointer to the the sink being added + /// @param[in] start: starts the source if start is true, otherwise delayed start void addSink(sink::SinkPtr sink, bool start = false); // Source and Sink + /// @brief Find a source by name + /// @param[in] name the name to find + /// @return A shared pointer to the source if found, otherwise nullptr source::SourcePtr findSource(const std::string &name) const { for (auto &s : m_sources) @@ -127,7 +177,9 @@ namespace mtconnect { return nullptr; } - + /// @brief Find a sink by name + /// @param name the name to find + /// @return A shared pointer to the sink if found, otherwise nullptr sink::SinkPtr findSink(const std::string &name) const { for (auto &s : m_sinks) @@ -137,22 +189,44 @@ namespace mtconnect { return nullptr; } + /// @brief Get the list of all sources + /// @return The list of all source in the agent const auto &getSources() const { return m_sources; } + /// @brief Get the list of all sinks + /// @return The list of all sinks in the agent const auto &getSinks() const { return m_sinks; } + /// @brief Get the MTConnect schema version the agent is supporting + /// @return The MTConnect schema version as a string const auto &getSchemaVersion() const { return m_schemaVersion; } - // Get device from device map + /// @brief Find a device by name + /// @param[in] name The name of the device to find + /// @return A shared pointer to the device DevicePtr getDeviceByName(const std::string &name); + /// @brief Find a device by name (Const Version) + /// @param[in] name The name of the device to find + /// @return A shared pointer to the device DevicePtr getDeviceByName(const std::string &name) const; + /// @brief Finds the device given either its UUID or its name + /// @param[in] idOrName The uuid or name of the device + /// @return A shared pointer to the device DevicePtr findDeviceByUUIDorName(const std::string &idOrName) const; + /// @brief Gets the list of devices + /// @return The list of devices owned by the caller const auto getDevices() const { std::list list; boost::push_back(list, m_deviceIndex); return list; } - DevicePtr defaultDevice() const + + /// @brief Get a pointer to the default device + /// + /// The default device is the first device that is not the Agent device. + /// + /// @return A shared pointer to the default device + DevicePtr getDefaultDevice() const { if (m_deviceIndex.size() > 0) { @@ -163,25 +237,65 @@ namespace mtconnect { return nullptr; } - DevicePtr getDefaultDevice() const { return defaultDevice(); } - - // Asset information + /// @deprecated use `getDefaultDevice()` instead + /// @brief Get a pointer to the default device + /// @return A shared pointer to the default device + /// @note Cover method for `getDefaultDevice()` + DevicePtr defaultDevice() const { return getDefaultDevice(); } + + /// @brief Get a pointer to the asset storage object + /// @return A pointer to the asset storage object asset::AssetStorage *getAssetStorage() { return m_assetStorage.get(); } - // Add the a device from a configuration file + /// @brief Add a device to the agent + /// @param[in] device The device to add. + /// @note This method is not fully implemented after agent initialization void addDevice(DevicePtr device); + /// @brief Updates a device's UUID and/or its name + /// @param[in] device The modified device + /// @param[in] oldUuid The old uuid + /// @param[in] oldName The old name void deviceChanged(DevicePtr device, const std::string &oldUuid, const std::string &oldName); + /// @brief Reload the devices from a device file after updates + /// @param deviceFile The device file to load + /// @return true if successful bool reloadDevices(const std::string &deviceFile); - // Message when adapter has connected and disconnected - void connecting(const std::string &adapter); - void disconnected(const std::string &adapter, const StringList &devices, bool autoAvailable); - void connected(const std::string &adapter, const StringList &devices, bool autoAvailable); - - // Message protocol command + /// @name Message when source has connected and disconnected + ///@{ + + /// @brief Called when source begins trying to connect + /// @param source The source identity + void connecting(const std::string &source); + /// @brief Called when source is disconnected + /// @param[in] source The source identity + /// @param[in] devices The list of devices associated with this source + /// @param[in] autoAvailable `true` if the source should automatically set available to + /// `UNAVAILABLE` + void disconnected(const std::string &source, const StringList &devices, bool autoAvailable); + /// @brief Called when source is connected + /// @param source The source identity + /// @param[in] devices The list of devices associated with this source + /// @param[in] autoAvailable `true` if the source should automatically set available to + /// `AVAILABLE` + void connected(const std::string &source, const StringList &devices, bool autoAvailable); + + ///@} + + /// @brief Called when a source receives a command from a data source + /// @param[in] device The device name associated with this source + /// @param[in] command The command being sent + /// @param[in] value The value of the command + /// @param[in] source The identity of the source void receiveCommand(const std::string &device, const std::string &command, const std::string &value, const std::string &source); + /// @brief Method to get a data item for a device + /// @param[in] deviceName The name or uuid of the device + /// @param[in] dataItemName The name or id of the data item + /// @return Shared pointer to the data item if found + /// @note Cover method for `findDeviceByUUIDorName()` and `DataItem::getDeviceDataItem()` from + /// the device. DataItemPtr getDataItemForDevice(const std::string &deviceName, const std::string &dataItemName) const { @@ -189,7 +303,9 @@ namespace mtconnect { return (dev) ? dev->getDeviceDataItem(dataItemName) : nullptr; } - // Find data items by name/id + /// @brief Get a data item by its id. + /// @param id Unique id of the data item + /// @return Shared pointer to the data item if found DataItemPtr getDataItemById(const std::string &id) const { auto diPos = m_dataItemMap.find(id); @@ -198,35 +314,92 @@ namespace mtconnect { return nullptr; } - // Pipeline methods + /// @name Pipeline related methods to receive data from sources + ///@{ + + /// @brief Receive an observation + /// @param[in] observation A shared pointer to the observation void receiveObservation(observation::ObservationPtr observation); + /// @brief Receive an asset + /// @param[in] asset A shared pointer to the asset void receiveAsset(asset::AssetPtr asset); + /// @brief Receive a device + /// @param[in] device A shared pointer to the device + /// @param[in] version If the DeviceXML should be versioned + /// @return If the device could be updated or added bool receiveDevice(device_model::DevicePtr device, bool version = true); + /// @brief Received an instruction from the source to remove an asset + /// @param[in] device Device name or uuid the asset belongs to + /// @param[in] id The asset id + /// @param[in] time The timestamp the remove occurred at + /// @return `true` if the asset was found and removed bool removeAsset(DevicePtr device, const std::string &id, const std::optional time = std::nullopt); + /// @brief Removes all assets for by device, type, or device and type + /// @param[in] device Optional device name or uuid + /// @param[in] type Optional type of asset + /// @param[in] time Timestamp of the observations + /// @param[out] list List of assets removed + /// @return `true` if any assets were found bool removeAllAssets(const std::optional device, const std::optional type, const std::optional time, asset::AssetList &list); + /// @brief Send asset removed observation when an asset is removed. + /// + /// Also sets asset changed to `UNAVAILABLE` if the asset removed asset was the last changed. + /// + /// @param device The device related to the asset + /// @param asset The asset void notifyAssetRemoved(DevicePtr device, const asset::AssetPtr &asset); - // Adapter feedback + ///@} + + /// @brief Method called by source when it cannot continue + /// @param identity identity of the source void sourceFailed(const std::string &identity); - // For testing + /// @name For testing + ///@{ + + /// @brief Returns a shared pointer to the agent device + /// @return shared pointer to the agent device auto getAgentDevice() { return m_agentDevice; } + ///@} - // Printers - printer::Printer *getPrinter(const std::string &aType) const + /// @brief Get a pointer to the printer for a mime type + /// @param type The mime type + /// @return pointer to the printer or nullptr if it does not exist + /// @note Currently `xml` and `json` are supported. + printer::Printer *getPrinter(const std::string &type) const { - auto printer = m_printers.find(aType); + auto printer = m_printers.find(type); if (printer != m_printers.end()) return printer->second.get(); else return nullptr; } + + /// @brief Get the map of available printers + /// @return A const reference to the printer map const auto &getPrinters() const { return m_printers; } - // Handle the device/path parameters for the xpath search + /// @brief Prefixes the path with the device and rewrites the composed + /// paths by repeating the prefix. The resulting path is valid + /// XPath. + /// + /// For example: a Device with a uuid="AAABBB" with the path + /// ``` + /// //DataItem[@type='EXECUTION']|//DataItem[@type='CONTROLLER_MODE'] + /// ``` + /// + /// Is rewritten: + /// ``` + /// //Devices/Device[@uuid="AAABBB"]//DataItem[@type='EXECUTION']|//Devices/Device[@uuid="AAABBB"]//DataItem[@type='CONTROLLER_MODE'] + /// ``` + /// + /// @param[in] path Optional path to prefix + /// @param[in] device Optional device if one device is specified + /// @return The rewritten path properly prefixed std::string devicesAndPath(const std::optional &path, const DevicePtr device) const; @@ -258,8 +431,8 @@ namespace mtconnect { boost::asio::io_context::strand m_strand; std::shared_ptr m_loopback; - - bool m_started { false }; + + bool m_started {false}; // Asset Management std::unique_ptr m_assetStorage; @@ -282,19 +455,25 @@ namespace mtconnect { // Agent Device device_model::AgentDevicePtr m_agentDevice; + /// @brief tag for Device multi-index unsorted struct BySeq {}; + /// @brief tag for Device multi-index by Name struct ByName {}; + /// @brief tag for Device multi-index by UUID struct ByUuid {}; + /// @brief Device UUID extractor for multi-index struct ExtractDeviceUuid { using result_type = std::string; const result_type &operator()(const DevicePtr &d) const { return *d->getUuid(); } result_type operator()(const DevicePtr &d) { return *d->getUuid(); } }; + + /// @brief Device name extractor for multi-index struct ExtractDeviceName { using result_type = std::string; @@ -302,7 +481,7 @@ namespace mtconnect { result_type operator()(DevicePtr &d) { return *d->getComponentName(); } }; - // Data containers + /// @brief Devuce multi-index using DeviceIndex = mic::multi_index_container< DevicePtr, mic::indexed_by>, mic::hashed_unique, ExtractDeviceUuid>, @@ -329,6 +508,7 @@ namespace mtconnect { configuration::HookManager m_beforeStopHooks; }; + /// @brief Association of the pipeline's interface to the `Agent` class AGENT_LIB_API AgentPipelineContract : public pipeline::PipelineContract { public: @@ -379,6 +559,7 @@ namespace mtconnect { return std::make_unique(this); } + /// @brief The sinks interface to the `Agent` class AGENT_LIB_API AgentSinkContract : public sink::SinkContract { public: @@ -400,7 +581,7 @@ namespace mtconnect { return m_agent->findDeviceByUUIDorName(idOrName); } const std::list getDevices() const override { return m_agent->getDevices(); } - DevicePtr defaultDevice() const override { return m_agent->defaultDevice(); } + DevicePtr getDefaultDevice() const override { return m_agent->getDefaultDevice(); } DataItemPtr getDataItemById(const std::string &id) const override { return m_agent->getDataItemById(id); diff --git a/src/mtconnect/asset/asset.hpp b/src/mtconnect/asset/asset.hpp index ce71319f..90ee36c3 100644 --- a/src/mtconnect/asset/asset.hpp +++ b/src/mtconnect/asset/asset.hpp @@ -27,14 +27,24 @@ #include "mtconnect/utilities.hpp" namespace mtconnect { + /// @brief Asset namespace namespace asset { class Asset; using AssetPtr = std::shared_ptr; using AssetList = std::list; + /// @brief An abstract MTConnect Asset + /// + /// The asset provides a common factory to create all known asset types. It can + /// support raw assets and convert unknown XML documents to entities. + /// + /// Each known asset type must register itself with the asset factory. class AGENT_LIB_API Asset : public entity::Entity { public: + /// @brief Abstract Asset constructor + /// @param name asset name, sometimes referred to as the asset type + /// @param props asset properties Asset(const std::string &name, const entity::Properties &props) : entity::Entity(name, props), m_removed(false) { @@ -43,11 +53,22 @@ namespace mtconnect { } ~Asset() override = default; + /// @brief an assets identity is its `assetId` property + /// @return the `assetId` const entity::Value &getIdentity() const override { return getProperty("assetId"); } + /// @brief get the static asset factory + /// @return shared pointer to the factory static entity::FactoryPtr getFactory(); + /// @brief get the root node of the asset hierarchy. This is the `Assets` entity. + /// @return shared pointer to the root factory static entity::FactoryPtr getRoot(); + /// @brief Sets a property of the asset + /// + /// Special handling of `removed`. If `true` sets the asset state to removed. + /// @param key property `key` + /// @param v property value void setProperty(const std::string &key, const entity::Value &v) override { entity::Value r = v; @@ -61,9 +82,18 @@ namespace mtconnect { m_properties.insert_or_assign(key, r); } + /// @brief Set a property + /// @param property the property void setProperty(const entity::Property &property) { Entity::setProperty(property); } + /// @brief Cover method for `getName()` const auto &getType() const { return getName(); } + + /// @brief gets the asset id + /// + /// Every asset must have an asset id. + /// @return the assets identity + /// @throws PropertyError if there is no assetId const std::string &getAssetId() const { if (m_assetId.empty()) @@ -76,12 +106,18 @@ namespace mtconnect { } return m_assetId; } + /// @brief Set the asset id + /// @param id the id void setAssetId(const std::string &id) { m_assetId = id; setProperty("assetId", id); } + /// @brief get the device uuid + /// + /// In version 1.8 and later, all assets must have a device uuid. + /// @return optional device uuid const std::optional getDeviceUuid() const { const auto &v = getProperty("deviceUuid"); @@ -90,6 +126,8 @@ namespace mtconnect { else return std::nullopt; } + /// @brief gets the timestamp if set + /// @return optional timestamp if available const std::optional getTimestamp() const { const auto &v = getProperty("timestamp"); @@ -99,13 +137,20 @@ namespace mtconnect { return std::nullopt; } bool isRemoved() const { return m_removed; } + /// @brief sets the removed state of the asset void setRemoved() { setProperty("removed", true); m_removed = true; } + /// @brief register the factory for an asset type + /// @param t the type or name of the asset + /// @param factory the factory to create assets static void registerAssetType(const std::string &t, entity::FactoryPtr factory); + /// @brief compares two asset ids + /// @param another other asset + /// @return `true` if they have the same asset id bool operator==(const Asset &another) const { return getAssetId() == another.getAssetId(); } protected: @@ -113,6 +158,8 @@ namespace mtconnect { bool m_removed; }; + /// @brief A simple `RAW` asset that just carries the data associated + /// with the top node. class AGENT_LIB_API ExtendedAsset : public Asset { public: diff --git a/src/mtconnect/asset/asset_buffer.hpp b/src/mtconnect/asset/asset_buffer.hpp index 41c53cd0..b4aa75bf 100644 --- a/src/mtconnect/asset/asset_buffer.hpp +++ b/src/mtconnect/asset/asset_buffer.hpp @@ -51,6 +51,7 @@ namespace mtconnect::asset { class AGENT_LIB_API AssetBuffer : public AssetStorage { public: + /// @brief Structure to store asset for boost multi index container struct AssetNode { AssetNode(AssetPtr &asset) : m_asset(asset), m_identity(asset->getAssetId()) {} @@ -86,15 +87,20 @@ namespace mtconnect::asset { using RemoveCountByType = std::unordered_map; using RemoveCountByDeviceAndType = std::unordered_map; + /// @brief Index by first in/first out sequence struct ByFifo {}; + /// @brief Index by assetId struct ByAssetId {}; + /// @brief Index by Device and Type struct ByDeviceAndType {}; + /// @brief Index by type struct ByType {}; + /// @brief The Multi-Index Container type using AssetIndex = mic::multi_index_container< AssetNode, mic::indexed_by< @@ -104,6 +110,8 @@ namespace mtconnect::asset { mic::key<&AssetNode::getDeviceUuid, &AssetNode::getType>>, mic::hashed_non_unique, mic::key<&AssetNode::getType>>>>; + /// @brief Create an asset buffer with a maximum size + /// @param max the maximum size AssetBuffer(size_t max) : AssetStorage(max) {} ~AssetBuffer() = default; diff --git a/src/mtconnect/asset/asset_storage.hpp b/src/mtconnect/asset/asset_storage.hpp index b3f2e6f7..a8c1e374 100644 --- a/src/mtconnect/asset/asset_storage.hpp +++ b/src/mtconnect/asset/asset_storage.hpp @@ -30,49 +30,131 @@ namespace mtconnect { namespace asset { + /// @brief Abstract asset storage + /// + /// Assets can be stored in memory or persisted. The agent uses + /// the `AssetStorage` to abstract storage and retrieval of + /// assets by assetId, type, and device. + /// + /// When an asset it is added or updated it moves to the beginning of the asset list. + /// Assets are deleted from storage when there are `max` assets in the buffer and another + /// is added. The oldest assets are removed first. + /// + /// Removal does not change the asset position and marks the asset as removed. class AGENT_LIB_API AssetStorage { public: + /// @brief A map of types to counts using TypeCount = std::map; + /// @brief Constructor with a max count + /// @param[in] max Max count can be max size_t if there are no limits to storage + /// in memory storage is assumed to be finite. AssetStorage(size_t max) : m_maxAssets(max) {} virtual ~AssetStorage() = default; + /// @brief Get the maximum number of assets that con be stored + /// @return The maximum number of assets. size_t getMaxAssets() const { return m_maxAssets; } + /// @brief Get the number of assets in storage + /// @param[in] active `true` if only assets that are not removed are counted + /// @return total count virtual size_t getCount(bool active = true) const = 0; + + /// @brief Get an associative array of counts by type + /// @param[in] active `true` if only assets that are not removed are counted + /// @return the counts by type virtual TypeCount getCountsByType(bool active = true) const = 0; - // Mutation + /// @name Create, Update, and Removal + ///@{ + + /// @brief add an asset to the storage + /// @param[in] asset an asset + /// @return shared pointer to the old asset if changed virtual AssetPtr addAsset(AssetPtr asset) = 0; + + /// @brief mark an asset as removed by assetId + /// @param[in] id the assetId + /// @param[in] time the timestamp for the removal + /// @return shared pointer to the removed asset if found virtual AssetPtr removeAsset(const std::string &id, const std::optional &time = std::nullopt) = 0; + /// @brief Remove assets by device and type + /// @param[out] list list of assets removed + /// @param[in] device optional device to filter assets + /// @param[in] type optional type to filter assets + /// @param[in] time optional timestamp, defaults to now + /// @return the number of assets removed + virtual size_t removeAll(AssetList &list, + const std::optional device = std::nullopt, + const std::optional type = std::nullopt, + const std::optional &time = std::nullopt) = 0; + ///@} + + /// @name Retrival + ///@{ - // Retrival + /// @brief get an asset by its assetId + /// @param[in] id the assetId + /// @return shared point to the asset if found virtual AssetPtr getAsset(const std::string &id) const = 0; + /// @brief get a list of assets with optional filters + /// @param[out] list returned list of assets + /// @param[in] max maximum number of assets to dind + /// @param[in] active `false` to skip removed assets + /// @param[in] device optional device uuid to select + /// @param[in] type optional type to select + /// @return the number of assets found virtual size_t getAssets(AssetList &list, size_t max, const bool active = true, const std::optional device = std::nullopt, const std::optional type = std::nullopt) const = 0; + /// @brief get a list of assets given a list of asset ids + /// @param[out] list list of assets + /// @param[in] ids assetIds to find + /// @return the number of assets found virtual size_t getAssets(AssetList &list, const std::list &ids) const = 0; + ///@} - // Count + /// @name Count related methods + ///@{ + + /// @brief gets counts of assets by device and type + /// @param[in] device a device uuid + /// @param[in] type the type of asset + /// @param[in] active `false` to skip removed assets + /// @return the number of assets virtual size_t getCountForDeviceAndType(const std::string &device, const std::string &type, bool active = true) const = 0; + + /// @brief get count of a type of asset for all devices + /// @param[in] type the type + /// @param[in] active `false` to skip removed assets + /// @return the number of assets virtual size_t getCountForType(const std::string &type, bool active = true) const = 0; + /// @brief get count of all types of assets for a device + /// @param[in] device the device uuid + /// @param[in] active `false` to skip removed assets + /// @return the number of assets virtual size_t getCountForDevice(const std::string &device, bool active = true) const = 0; - + /// @brief get the count by types for a device + /// @param[in] device the device uuid + /// @param[in] active `false` to skip removed assets + /// @return a map of types and their counts virtual TypeCount getCountsByTypeForDevice(const std::string &device, bool active = true) const = 0; + ///@} - // Bulk remove - virtual size_t removeAll(AssetList &list, - const std::optional device = std::nullopt, - const std::optional type = std::nullopt, - const std::optional &time = std::nullopt) = 0; + /// @name For mutex locking + ///@{ - // For mutex locking + /// @brief lock the storage auto lock() { return m_bufferLock.lock(); } + /// @brief unlock the storage auto unlock() { return m_bufferLock.unlock(); } + /// @brief try to lock the storage auto try_lock() { return m_bufferLock.try_lock(); } + ///@} protected: // Access control to the buffer diff --git a/src/mtconnect/asset/cutting_tool.hpp b/src/mtconnect/asset/cutting_tool.hpp index 6f090fa7..1c3e820c 100644 --- a/src/mtconnect/asset/cutting_tool.hpp +++ b/src/mtconnect/asset/cutting_tool.hpp @@ -27,6 +27,7 @@ namespace mtconnect { namespace asset { + /// @brief Cutting Tool Architype Asset class AGENT_LIB_API CuttingToolArchetype : public Asset { public: @@ -34,6 +35,7 @@ namespace mtconnect { static void registerAsset(); }; + /// @brief Cutting Tool Asset class AGENT_LIB_API CuttingTool : public Asset { public: diff --git a/src/mtconnect/asset/file_asset.hpp b/src/mtconnect/asset/file_asset.hpp index 35d5c5c3..e8daee8c 100644 --- a/src/mtconnect/asset/file_asset.hpp +++ b/src/mtconnect/asset/file_asset.hpp @@ -29,6 +29,7 @@ namespace mtconnect { namespace asset { + /// @brief File Archetype Asset class AGENT_LIB_API FileArchetypeAsset : public entity::Entity { public: @@ -36,6 +37,7 @@ namespace mtconnect { static void registerAsset(); }; + /// @brief File Asset class AGENT_LIB_API FileAsset : public FileArchetypeAsset { public: diff --git a/src/mtconnect/asset/qif_document.hpp b/src/mtconnect/asset/qif_document.hpp index ca7d61a9..e1c58fcf 100644 --- a/src/mtconnect/asset/qif_document.hpp +++ b/src/mtconnect/asset/qif_document.hpp @@ -28,6 +28,9 @@ #include "mtconnect/utilities.hpp" namespace mtconnect::asset { + /// @brief QIF Document Wrapper + /// + /// Contains a QIF Document without validating the contents. class AGENT_LIB_API QIFDocumentWrapper : public Asset { public: diff --git a/src/mtconnect/asset/raw_material.hpp b/src/mtconnect/asset/raw_material.hpp index 0fc60aeb..2612ed5d 100644 --- a/src/mtconnect/asset/raw_material.hpp +++ b/src/mtconnect/asset/raw_material.hpp @@ -28,6 +28,7 @@ #include "mtconnect/utilities.hpp" namespace mtconnect::asset { + /// @brief A raw material asset class AGENT_LIB_API RawMaterial : public Asset { public: diff --git a/src/mtconnect/buffer/checkpoint.hpp b/src/mtconnect/buffer/checkpoint.hpp index f5dca83a..e2888e0a 100644 --- a/src/mtconnect/buffer/checkpoint.hpp +++ b/src/mtconnect/buffer/checkpoint.hpp @@ -27,26 +27,57 @@ #include "mtconnect/observation/observation.hpp" #include "mtconnect/utilities.hpp" +/// @brief Internal storage of observations namespace mtconnect::buffer { + /// @brief A point in time snapshot of all data items with a optional filter class AGENT_LIB_API Checkpoint { public: + /// @brief create an empty checkpoint Checkpoint() = default; + + /// @brief Copy constructor for a checkpoint + /// @param[in] checkpoint the previous checkpoint + /// @param[in] filterSet an optional set of data item ids for filtering Checkpoint(const Checkpoint &checkpoint, const FilterSetOpt &filterSet = std::nullopt); ~Checkpoint(); - void addObservation(observation::ObservationPtr event); - bool dataSetDifference(observation::ObservationPtr event) const; + /// @brief Add an observation to the checkpoint + /// @param[in] observation an observation + void addObservation(observation::ObservationPtr observation); + + /// @brief If this is a data set event, diff the value + /// @param[in] observation the data set observation + /// @return `true` if the data set changed + bool dataSetDifference(observation::ObservationPtr observation) const; + + /// @brief copy another checkpoint to this checkpoint + /// @param[in] checkpoint a checkpoint to copy + /// @param[in] filterSet an optional filter set void copy(Checkpoint const &checkpoint, const FilterSetOpt &filterSet = std::nullopt); + + /// @brief clear the contents of this checkpoint void clear(); + + /// @brief Add a filter to the checkpoint void filter(const FilterSet &filterSet); + /// @brief does this checkpoint have a filter? + /// @return `true` if a checkpoint exists bool hasFilter() const { return bool(m_filter); } + /// @brief get a map of data item id to observation shared pointers + /// @return a map of ids to observations const std::unordered_map &getObservations() const { return m_observations; } + /// @brief updates the data item reference of an observation in a checkpoint + /// + /// Used when the device model is modified and data items may have been removed or + /// changed. The new data item shared pointer will replace the old. + /// + /// @param[in] diMap the map of data ids to data item pointers void updateDataItems(std::unordered_map &diMap) { for (auto &o : m_observations) @@ -55,10 +86,16 @@ namespace mtconnect::buffer { } } + /// @brief Get a list of observations from the checkpoint + /// @param[in,out] list the list to add the observations to + /// @param[in] filter an optional filter for the observations void getObservations(observation::ObservationList &list, const FilterSetOpt &filter = std::nullopt) const; - observation::ObservationPtr getObservation(const std::string &id) + /// @brief Get an observation for a data item id + /// @param[in] id the data item id + /// @return shared pointer to the observation if it exists + observation::ObservationPtr getObservation(const std::string &id) const { auto pos = m_observations.find(id); if (pos != m_observations.end()) diff --git a/src/mtconnect/buffer/circular_buffer.hpp b/src/mtconnect/buffer/circular_buffer.hpp index 06c63951..15d61437 100644 --- a/src/mtconnect/buffer/circular_buffer.hpp +++ b/src/mtconnect/buffer/circular_buffer.hpp @@ -31,9 +31,13 @@ namespace mtconnect::buffer { using SequenceNumber_t = uint64_t; + /// @brief Limited epherimal in-memory storage of observations and checkpoint management class AGENT_LIB_API CircularBuffer { public: + /// @brief Create a circular buffer + /// @param bufferSize the size of the circular buffer + /// @param checkpointFreq how often to create checkpoints CircularBuffer(unsigned int bufferSize, int checkpointFreq) : m_sequence(1ull), m_firstSequence(m_sequence), @@ -46,6 +50,9 @@ namespace mtconnect::buffer { ~CircularBuffer() { m_checkpoints.clear(); } + /// @brief get an observation at a sequence number + /// @param seq the sequence number + /// @return shared pointer to an obseration at sequence observation::ObservationPtr getFromBuffer(uint64_t seq) const { auto off = seq - m_firstSequence; @@ -55,14 +62,24 @@ namespace mtconnect::buffer { return observation::ObservationPtr(); } - auto getIndexAt(uint64_t at) { return at - m_firstSequence; } + /// @brief get index into underlying curcular buffer at a sequence number + /// @param at the sequence number + /// @return the index into the circular buffer + auto getIndexAt(uint64_t at) const { return at - m_firstSequence; } + /// @brief Get the current sequence number + /// @return sequence number one greater than last observation in circular buffer SequenceNumber_t getSequence() const { return m_sequence; } - + /// @brief get the buffer size + /// @return the buffer size unsigned int getBufferSize() const { return m_slidingBufferSize; } + /// @brief get the first sequence number in the circular buffer + /// @return first sequence SequenceNumber_t getFirstSequence() const { return m_firstSequence; } + /// @brief update the data item references when device model changes + /// @param diMap the map of data item ids to new data item entities void updateDataItems(std::unordered_map &diMap) { for (auto &o : m_slidingBuffer) @@ -79,6 +96,11 @@ namespace mtconnect::buffer { } } + /// @brief Set the sequence number + /// + /// recomputes the first sequence if the sequence is larger than the circular buffer size. + /// + /// @param seq the new sequence number void setSequence(SequenceNumber_t seq) { m_sequence = seq; @@ -86,6 +108,12 @@ namespace mtconnect::buffer { m_firstSequence = seq - m_slidingBuffer.size(); } + /// @brief Add an observation to the circular buffer + /// - Diffs the data set if the observation is a data set + /// - Sets the observation sequence number + /// + /// @param observation the observation + /// @return the sequence number of the observation SequenceNumber_t addToBuffer(observation::ObservationPtr &observation) { if (observation->isOrphan()) @@ -133,13 +161,24 @@ namespace mtconnect::buffer { return seq; } - // Checkpoint - Checkpoint &getLatest() { return m_latest; } - Checkpoint &getFirst() { return m_first; } - auto getCheckoointFreq() { return m_checkpointFreq; } - auto getCheckpointCount() { return m_checkpointCount; } - - std::unique_ptr getCheckpointAt(SequenceNumber_t at, const FilterSetOpt &filterSet) + /// @name Checkpoint methods + ///@{ + + /// @brief Get the checkpoint at the end of the circular buffer + /// @return reference to the checkpoint + const Checkpoint &getLatest() const { return m_latest; } + /// @brief Get the checkpoint at the beginning of the circular buffer + /// @return reference to the checkpoint + const Checkpoint &getFirst() const { return m_first; } + auto getCheckpointFreq() const { return m_checkpointFreq; } + auto getCheckpointCount() const { return m_checkpointCount; } + + /// @brief Get a checkpoint at a sequence number + /// @param at the sequence number to get the checkpoint at + /// @param filterSet the filter to apply to the new checkpoint + /// @return a unique point to a new checkpoint + std::unique_ptr getCheckpointAt(SequenceNumber_t at, + const FilterSetOpt &filterSet) const { std::lock_guard lock(m_sequenceLock); @@ -184,11 +223,21 @@ namespace mtconnect::buffer { return check; } - + ///@} + + /// @brief Get a list of observations from the curcular buffer + /// @param[in] count maximum number of observations to get + /// @param[in] filterSet optional filter set of data item ids + /// @param[in] start optional starting sequence + /// @param[in] to optional ending sequence + /// @param[out] end last sequence number in the list + /// @param[out] firstSeq first sequence number in the list + /// @param[out] endOfBuffer `true` if the last sequence is at the end of the buffer + /// @return unique pointer to a list of shared observation pointers std::unique_ptr getObservations( int count, const FilterSetOpt &filterSet, const std::optional start, const std::optional to, SequenceNumber_t &end, SequenceNumber_t &firstSeq, - bool &endOfBuffer) + bool &endOfBuffer) const { auto results = std::make_unique(); @@ -253,14 +302,20 @@ namespace mtconnect::buffer { return results; } - // For mutex locking + /// @name Mutex lock management + ///@{ + + /// @brief lock the mutex auto lock() { return m_sequenceLock.lock(); } + /// @brief unlock the mutex auto unlock() { return m_sequenceLock.unlock(); } + /// @brief try to lock the mutex auto try_lock() { return m_sequenceLock.try_lock(); } + ///@} protected: // Access control to the buffer - std::recursive_mutex m_sequenceLock; + mutable std::recursive_mutex m_sequenceLock; // Sequence number SequenceNumber_t m_sequence; diff --git a/src/mtconnect/config.hpp b/src/mtconnect/config.hpp index c5fa269a..8d40f80e 100644 --- a/src/mtconnect/config.hpp +++ b/src/mtconnect/config.hpp @@ -1,7 +1,7 @@ #pragma once // -// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) +// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,8 @@ // limitations under the License. // -#pragma once +/// @file config.hpp +/// @brief common includes and cross platform requirements #include diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index 2b86e51e..6a54fccd 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -404,7 +404,7 @@ namespace mtconnect::configuration { LOG(info) << "Agent Configuration stopped"; } - DevicePtr AgentConfiguration::defaultDevice() { return m_agent->defaultDevice(); } + DevicePtr AgentConfiguration::getDefaultDevice() { return m_agent->getDefaultDevice(); } void AgentConfiguration::setLoggingLevel(const logr::trivial::severity_level level) { @@ -812,7 +812,7 @@ namespace mtconnect::configuration { if (!device) { LOG(warning) << "Cannot locate device name '" << deviceName << "', trying default"; - device = defaultDevice(); + device = getDefaultDevice(); if (device) { deviceName = *device->getComponentName(); @@ -884,7 +884,7 @@ namespace mtconnect::configuration { } } } - else if ((device = defaultDevice())) + else if ((device = getDefaultDevice())) { ConfigOptions adapterOptions {options}; diff --git a/src/mtconnect/configuration/agent_config.hpp b/src/mtconnect/configuration/agent_config.hpp index e726a459..6c912bbd 100644 --- a/src/mtconnect/configuration/agent_config.hpp +++ b/src/mtconnect/configuration/agent_config.hpp @@ -60,9 +60,13 @@ namespace mtconnect { #endif class XmlPrinter; + + /// @brief Configuration namespace namespace configuration { using DevicePtr = std::shared_ptr; + /// @brief Parses the configuration file and creates the `Agent`. Manages config + /// file tracking and restarting of the agent. class AGENT_LIB_API AgentConfiguration : public MTConnectService { public: @@ -71,49 +75,113 @@ namespace mtconnect { using ptree = boost::property_tree::ptree; + /// @brief Construct the agent configuration AgentConfiguration(); virtual ~AgentConfiguration(); - // Hooks + /// @name Callbacks for initialization phases + ///@{ + + /// @brief Get the callback manager after the agent is created + /// @return the callback manager auto &afterAgentHooks() { return m_afterAgentHooks; } + /// @brief Get the callback manager after the agent is started + /// @return the callback manager auto &beforeStartHooks() { return m_beforeStartHooks; } + /// @brief Get the callback manager after the agent is stopped + /// @return the callback manager auto &beforeStopHooks() { return m_beforeStopHooks; } + ///@} - // For MTConnectService + /// @brief stops the agent. Used in daemons. void stop() override; + /// @brief starts the agent. Used in daemons. void start() override; + /// @brief initializes the configuration of the agent from the command line parameters + /// @param[in] options command line parameters void initialize(const boost::program_options::variables_map &options) override; + /// @brief Configure the logger with the config node from the config file + /// @param config the configuration node void configureLogger(const ptree &config); + /// @brief load a configuration file + /// @param[in] file name of the file void loadConfig(const std::string &file); + /// @brief assign the agent associated with this configuration + /// @param[in] agent the agent the configuration will take ownership of void setAgent(std::unique_ptr &agent) { m_agent = std::move(agent); } + /// @brief get the agent associated with the configuration const Agent *getAgent() const { return m_agent.get(); } + /// @brief get the boost asio io context auto &getContext() { return m_context->getContext(); } + /// @brief get a pointer to the async io manager auto &getAsyncContext() { return *m_context.get(); } + /// @brief sets the path for the working directory to the crrent path void updateWorkingDirectory() { m_working = std::filesystem::current_path(); } + /// @name Configuration factories + ///@{ + /// @brief get the factory for creating sinks + /// @return the factory auto &getSinkFactory() { return m_sinkFactory; } + /// @brief get the factory for creating sources + /// @return the factory auto &getSourceFactory() { return m_sourceFactory; } + ///@} + + /// @brief get the pipeline context for this configuration + /// @note set after the agent is created auto getPipelineContext() { return m_pipelineContext; } + /// @name Logging methods + ///@{ + /// @brief gets the boost log sink + /// @return boost log sink const auto &getLoggerSink() const { return m_sink; } + /// @brief gets the log directory + /// @return log directory const auto &getLogDirectory() const { return m_logDirectory; } + /// @brief get the logging file name + /// @return log file name const auto &getLogFileName() const { return m_logFileName; } + /// @brief for log rolling, get the log archive pattern + /// @return log archive pattern const auto &getLogArchivePattern() const { return m_logArchivePattern; } + /// @brief Get the maximum size of all the log files + /// @return the maxumum size of all log files auto getMaxLogFileSize() const { return m_maxLogFileSize; } + /// @brief the maximum size of a log file when it triggers rolling over + /// @return the maxumum site of a log file auto getLogRotationSize() const { return m_logRotationSize; } + /// @brief How often to roll over the log file + /// + /// One of: + /// - `DAILY` + /// - `WEEKLY` + /// - `NEVER` + /// + /// @return the log file interval auto getRotationLogInterval() const { return m_rotationLogInterval; } + /// @brief Get the current log level + /// @return log level auto getLogLevel() const { return m_logLevel; } + /// @brief set the logging level + /// @param[in] level the new logging level void setLoggingLevel(const boost::log::trivial::severity_level level); + /// @brief Set the logging level as a string + /// @param level the new logging level + /// @return the logging level boost::log::trivial::severity_level setLoggingLevel(const std::string &level); + /// @brief get a pointer to the logger boost::log::trivial::logger_type *getLogger() { return m_logger; } + ///@} protected: - DevicePtr defaultDevice(); + DevicePtr getDefaultDevice(); void loadAdapters(const ptree &tree, const ConfigOptions &options); void loadSinks(const ptree &sinks, ConfigOptions &options); diff --git a/src/mtconnect/configuration/async_context.hpp b/src/mtconnect/configuration/async_context.hpp index 23de0049..31f35c3f 100644 --- a/src/mtconnect/configuration/async_context.hpp +++ b/src/mtconnect/configuration/async_context.hpp @@ -25,23 +25,32 @@ namespace mtconnect::configuration { - // Manages the boost asio context and allows for a syncronous - // callback to execute when all the worker threads have stopped. + /// @brief Manages the boost asio context and allows for a syncronous + /// callback to execute when all the worker threads have stopped. class AGENT_LIB_API AsyncContext { public: using SyncCallback = std::function; using WorkGuard = boost::asio::executor_work_guard; + /// @brief creates an asio context and a guard to prevent it from + /// stopping AsyncContext() { m_guard.emplace(m_context.get_executor()); } + /// @brief removes the copy constructor AsyncContext(const AsyncContext &) = delete; ~AsyncContext() {} + /// @brief get the boost asio context reference auto &getContext() { return m_context; } + /// @brief operator() returns a reference to the io context + /// @return the io context operator boost::asio::io_context &() { return m_context; } + /// @brief sets the number of theads for asio thread pool + /// @param[in] threads number of threads void setThreadCount(int threads) { m_threadCount = threads; } + /// @brief start `m_threadCount` worker threads void start() { m_running = true; @@ -84,6 +93,10 @@ namespace mtconnect::configuration { } while (m_running); } + /// @brief pause the worker threads. Sets a callback when the threads are paused. + /// @param[in] callback the callback to call + /// @param[in] safeStop stops by resetting the the guard, otherwise stop the + /// io context void pause(SyncCallback callback, bool safeStop = false) { m_paused = true; @@ -94,6 +107,8 @@ namespace mtconnect::configuration { m_context.stop(); } + /// @brief stop the worker threads + /// @param safeStop if `true` resets the guard or stops the context void stop(bool safeStop = true) { m_running = false; @@ -103,6 +118,7 @@ namespace mtconnect::configuration { m_context.stop(); } + /// @brief restarts the worker threads when paused void restart() { m_paused = false; diff --git a/src/mtconnect/configuration/config_options.hpp b/src/mtconnect/configuration/config_options.hpp index 9bd98101..7116d858 100644 --- a/src/mtconnect/configuration/config_options.hpp +++ b/src/mtconnect/configuration/config_options.hpp @@ -17,16 +17,29 @@ #pragma once +/// @file config_options.hpp +/// @brief Contains all the known configuration options + #include "mtconnect/config.hpp" namespace mtconnect { namespace configuration { + +/// @brief creates an const char * from the name as a string +/// +/// stringizes `name` +/// +/// @param name name of configuration parameter #define DECLARE_CONFIGURATION(name) inline const char *name = #name; - // Global + + /// @name Global Configuration Options + ///@{ DECLARE_CONFIGURATION(ExecDirectory); DECLARE_CONFIGURATION(WorkingDirectory); + ///@} - // Agent Configuration + /// @name Agent Configuration + ///@{ DECLARE_CONFIGURATION(DisableAgentDevice); DECLARE_CONFIGURATION(AllowPut); DECLARE_CONFIGURATION(AllowPutFrom); @@ -57,8 +70,10 @@ namespace mtconnect { DECLARE_CONFIGURATION(TlsVerifyClientCertificate); DECLARE_CONFIGURATION(VersionDeviceXmlUpdates); DECLARE_CONFIGURATION(WorkerThreads); + ///@} - // MQTT Configuration + /// @name MQTT Configuration + ///@{ DECLARE_CONFIGURATION(DeviceTopic); DECLARE_CONFIGURATION(AssetTopic); DECLARE_CONFIGURATION(ObservationTopic); @@ -72,8 +87,10 @@ namespace mtconnect { DECLARE_CONFIGURATION(MqttConnectInterval); DECLARE_CONFIGURATION(MqttUserName); DECLARE_CONFIGURATION(MqttPassword); + ///@} - // Adapter Configuration + /// @name Adapter Configuration + ///@{ DECLARE_CONFIGURATION(AdapterIdentity); DECLARE_CONFIGURATION(AdditionalDevices); DECLARE_CONFIGURATION(AutoAvailable); @@ -103,6 +120,7 @@ namespace mtconnect { DECLARE_CONFIGURATION(UpcaseDataItemValue); DECLARE_CONFIGURATION(Url); DECLARE_CONFIGURATION(UsePolling); + ///@} } // namespace configuration } // namespace mtconnect diff --git a/src/mtconnect/configuration/hook_manager.hpp b/src/mtconnect/configuration/hook_manager.hpp index 608f208a..4c62cc04 100644 --- a/src/mtconnect/configuration/hook_manager.hpp +++ b/src/mtconnect/configuration/hook_manager.hpp @@ -24,6 +24,9 @@ #include "mtconnect/config.hpp" namespace mtconnect::configuration { + + /// @brief Manages a list of callbacks. Callbacks can be named. + /// @tparam The type of the object reference to pass to the callback. template class HookManager { @@ -32,44 +35,64 @@ namespace mtconnect::configuration { using HookEntry = std::pair, Hook>; using HookList = std::list; + /// @brief Create a hook manager HookManager() {} ~HookManager() {} - // Add hooks without name, cannot be removed + /// @brief Add a hook to the end of the list as a rvalue without a name + /// @param[in] hook The callback void add(Hook &hook) { m_hooks.emplace_back(std::make_pair(std::nullopt, hook)); } + /// @brief Add a hook to the end of the list as a lvalue without a name + /// @param[in] hook The callback void add(Hook &&hook) { m_hooks.emplace_back(std::make_pair(std::nullopt, std::move(hook))); } + /// @brief Add a hook to the beginning of the list as a rvalue without a name + /// @param[in] hook The callback void addFirst(Hook &hook) { m_hooks.emplace_front(std::make_pair(std::nullopt, hook)); } + /// @brief Add a hook to the beginning of the list as a lvalue without a name + /// @param[in] hook The callback void addFirst(Hook &&hook) { m_hooks.emplace_front(std::make_pair(std::nullopt, std::move(hook))); } - // Add by name + /// @brief Add a hook to the end of the list as a rvalue + /// @param[in] name The name of the callback + /// @param[in] hook The callback void add(std::string &name, Hook &hook) { m_hooks.emplace_back(std::make_pair(std::nullopt, hook)); } + /// @brief Add a hook to the end of the list as a rvalue + /// @param[in] name The name of the callback + /// @param[in] hook The callback void add(std::string &name, Hook &&hook) { m_hooks.emplace_back(std::make_pair(std::nullopt, std::move(hook))); } + /// @brief Add a hook to the beginning of the list as a rvalue + /// @param[in] name The name of the callback + /// @param[in] hook The callback void addFirst(std::string &name, Hook &hook) { m_hooks.emplace_front(std::make_pair(std::nullopt, hook)); } + /// @brief Add a hook to the beginning of the list as a lvalue + /// @param[in] name The name of the callback + /// @param[in] hook The callback void addFirst(std::string &name, Hook &&hook) { m_hooks.emplace_front(std::make_pair(std::nullopt, std::move(hook))); } - // Remove by name + /// @brief remove a named callback from the list bool remove(const std::string &name) { auto v = m_hooks.remove_if([&name](const auto &v) { return v.first && *v.first == name; }); return v > 0; } - // Execute hooks9i + /// @brief call each of the hooks in order with an object + /// @param obj the object to pass to each callback void exec(T &obj) const { for (const auto &h : m_hooks) diff --git a/src/mtconnect/configuration/parser.cpp b/src/mtconnect/configuration/parser.cpp index b36735d1..2d2e1d1e 100644 --- a/src/mtconnect/configuration/parser.cpp +++ b/src/mtconnect/configuration/parser.cpp @@ -77,6 +77,7 @@ using namespace std; namespace mtconnect { namespace configuration { + /// @brief Actions for the configuration parser in reductions namespace ConfigurationParserActions { inline static void property(pair &t, const std::string &f, const std::string &s) @@ -115,6 +116,9 @@ namespace mtconnect { BOOST_PHOENIX_ADAPT_FUNCTION(void, start, ConfigurationParserActions::start, 2); BOOST_PHOENIX_ADAPT_FUNCTION(size_t, pos, ConfigurationParserActions::pos, 1); + /// @brief boost Spirit parser + /// @tparam It iterator type for consuming text + /// @tparam Skipper skip whitespace by default template class ConfigParser : public qi::grammar { diff --git a/src/mtconnect/configuration/parser.hpp b/src/mtconnect/configuration/parser.hpp index 2786c481..97e21ee8 100644 --- a/src/mtconnect/configuration/parser.hpp +++ b/src/mtconnect/configuration/parser.hpp @@ -25,15 +25,22 @@ namespace mtconnect { namespace configuration { + + /// @brief Error from configuration file parser. Just a runtime error. class AGENT_LIB_API ParseError : public std::runtime_error { public: using std::runtime_error::runtime_error; }; + /// @brief Configuration file parser struct AGENT_LIB_API Parser { + /// @brief Parse text string to a property tree + /// @param[in] text text to be parsed static boost::property_tree::ptree parse(const std::string &text); + /// @brief Parse file to a property tree + /// @param[in] path file to be parsed static boost::property_tree::ptree parse(const std::filesystem::path &path); }; } // namespace configuration diff --git a/src/mtconnect/configuration/service.hpp b/src/mtconnect/configuration/service.hpp index a5cf7ac7..b61b67a8 100644 --- a/src/mtconnect/configuration/service.hpp +++ b/src/mtconnect/configuration/service.hpp @@ -28,24 +28,47 @@ namespace mtconnect { namespace configuration { + /// @brief Abstract service supporting running as a Windows service or *NIX daemon class AGENT_LIB_API MTConnectService { public: MTConnectService() = default; virtual ~MTConnectService() = default; + /// @brief command line parser and entry point for agent + /// @param[in] argc the count of arguments + /// @param[in] argv the arguments virtual int main(int argc, char const *argv[]); + /// @brief initialize the service with the parser command line options virtual void initialize(const boost::program_options::variables_map &options) = 0; + /// @brief stop the srvice virtual void stop() = 0; + /// @brief start the service virtual void start() = 0; + /// @brief set the name of the service + /// @param[in] name name of the service void setName(std::string const &name) { m_name = name; } + /// @brief get the name of the service + /// @return service name std::string const &name() const { return m_name; } + /// @brief set the debugging state + /// @param debug `true` if debugging void setDebug(bool debug) { m_isDebug = debug; } + /// @brief get the debugging state + /// @return `true` if debugging bool getDebug() { return m_isDebug; } + /// @brief write out usage text to standard out + /// @param ec the exit code virtual void usage(int ec = 0); + /// @brief Parse command line options + /// @param argc number of optons + /// @param argv option values + /// @param command optonal command for testing + /// @param config optional configration file for testing + /// @return boost variable map boost::program_options::variables_map parseOptions(int argc, const char *argv[], boost::optional &command, boost::optional &config); diff --git a/src/mtconnect/device_model/agent_device.hpp b/src/mtconnect/device_model/agent_device.hpp index de62dabf..d02b0a07 100644 --- a/src/mtconnect/device_model/agent_device.hpp +++ b/src/mtconnect/device_model/agent_device.hpp @@ -32,14 +32,18 @@ namespace mtconnect { } // namespace source::adapter namespace device_model { + /// @brief Agent Device entity class AGENT_LIB_API AgentDevice : public Device { public: - // Constructor that sets variables from an attribute map + /// @brief Constructor that sets variables from an attribute map + /// + /// Should not be used directly, always create using the factory AgentDevice(const std::string &name, entity::Properties &props); ~AgentDevice() override = default; static entity::FactoryPtr getFactory(); + /// @brief initialize the agent device and add the device changed and removed data items void initialize() override { addRequiredDataItems(); @@ -48,12 +52,19 @@ namespace mtconnect { Device::initialize(); } + /// @brief Add an adapter and create a component to track it + /// @param adapter the adapter void addAdapter(const source::adapter::AdapterPtr adapter); + /// @brief get the connection status data item for an addapter + /// @param adapter the adapter name + /// @return shared pointer to the data item DataItemPtr getConnectionStatus(const std::string &adapter) { return getDeviceDataItem(adapter + "_connection_status"); } + /// @brief Get all the adapter components + /// @return shared pointer to the adapters component auto &getAdapters() { return m_adapters; } protected: diff --git a/src/mtconnect/device_model/component.hpp b/src/mtconnect/device_model/component.hpp index ee79b216..fb91fce6 100644 --- a/src/mtconnect/device_model/component.hpp +++ b/src/mtconnect/device_model/component.hpp @@ -43,9 +43,14 @@ namespace mtconnect { using DevicePtr = std::shared_ptr; using DataItemPtr = std::shared_ptr; + + /// @brief MTConnect Component Entity class AGENT_LIB_API Component : public entity::Entity { public: + /// @brief Create a component with a type and properties + /// @param[in] name the name of the component (o type) + /// @param[in] props properties of the component Component(const std::string &name, const entity::Properties &props); static ComponentPtr make(const std::string &name, const entity::Properties &props, entity::ErrorList &errors) @@ -56,22 +61,32 @@ namespace mtconnect { } static entity::FactoryPtr getFactory(); + + /// @brief get the shared pointer for the component + /// @return shared pointer auto getptr() const { return std::dynamic_pointer_cast(Entity::getptr()); } + /// @brief associate compositions and data items with the component virtual void initialize() { connectCompositions(); connectDataItems(); } - // Virtual destructor virtual ~Component(); - // Getter methods for the component ID/Name + /// @brief get the component id + /// @return component id const auto &getId() const { return m_id; } + /// @brief get the name property of the component + /// @return name if it exists const auto &getComponentName() const { return m_name; } + /// @brief get the component uuid + /// @return uuid if it exists const auto &getUuid() const { return m_uuid; } + /// @brief Get the topic name for the component + /// @return topic name virtual const std::string getTopicName() const { if (!m_topicName) @@ -86,6 +101,8 @@ namespace mtconnect { return *m_topicName; } + /// @brief get the description entity + /// @return shared pointer description entity::EntityPtr getDescription() { if (hasProperty("Description")) @@ -100,43 +117,55 @@ namespace mtconnect { } } + /// @brief set the manufacturer in the description + /// @param value the manufacturer void setManufacturer(const std::string &value) { auto desc = getDescription(); desc->setProperty("manufacturer", value); } + /// @brief set the station in the description + /// @param value the description void setStation(const std::string &value) { auto desc = getDescription(); desc->setProperty("station", value); } + /// @brief set the serial number in the description + /// @param value the serial number void setSerialNumber(const std::string &value) { auto desc = getDescription(); desc->setProperty("serialNumber", value); } + /// @brief set the description value in the description + /// @param value the description value void setDescriptionValue(const std::string &value) { auto desc = getDescription(); desc->setValue(value); } - // Setter methods + /// @brief set the uuid + /// @param uuid the uuid void setUuid(const std::string &uuid) { m_uuid = uuid; setProperty("uuid", uuid); } + /// @brief set the compoent name property, not the compoent type + /// @param name name property void setComponentName(const std::string &name) { m_name = name; setProperty("name", name); } - // Get the device that any component is associated with + /// @brief get the device (top level component) + /// @return shared pointer to the device virtual DevicePtr getDevice() const { DevicePtr device = m_device.lock(); @@ -152,11 +181,16 @@ namespace mtconnect { return device; } - // Set/Get the component's parent component + /// @brief get the parent component + /// @return shared pointer to the parent component ComponentPtr getParent() const { return m_parent.lock(); } - // Add to/get the component's std::list of children + /// @brief get the children of this component + /// @return list of component pointers auto getChildren() const { return getList("Components"); } + /// @brief add a child to this component + /// @param[in] child the child component + /// @param[in,out] errors errors that occurred when adding the child void addChild(ComponentPtr child, entity::ErrorList &errors) { addToList("Components", Component::getFactory(), child, errors); @@ -166,21 +200,35 @@ namespace mtconnect { child->buildDeviceMaps(device); } - // Add to/get the component's std::list of data items + /// @brief add a data item to the component + /// @param[in] dataItem the data item + /// @param[in,out] errors errors that occurred when adding the data item virtual void addDataItem(DataItemPtr dataItem, entity::ErrorList &errors); + /// @brief get the list of data items + /// @return the data item list auto getDataItems() const { return getList("DataItems"); } bool operator<(const Component &comp) const { return m_id < comp.getId(); } bool operator==(const Component &comp) const { return m_id == comp.getId(); } - // References + /// @brief connected references by looking them up in the device + /// @param device the device to use as an index void resolveReferences(DevicePtr device); - // Connect data items + /// @brief connect data items to this component void connectDataItems(); + /// @brief connect compositions to this component void connectCompositions(); + /// @brief index components to data items in the device + /// + /// Also builds the topics. + /// + /// @param device the device void buildDeviceMaps(DevicePtr device); + /// @brief get the composition by its id + /// @param id the composition id + /// @return shared pointer to the composition CompositionPtr getComposition(const std::string &id) const { auto comps = getList("Compositions"); @@ -220,6 +268,7 @@ namespace mtconnect { std::optional m_topicName; }; + /// @brief Comparison lambda to sort components struct ComponentComp { bool operator()(const Component *lhs, const Component *rhs) const { return *lhs < *rhs; } diff --git a/src/mtconnect/device_model/composition.hpp b/src/mtconnect/device_model/composition.hpp index 386ec6d9..3ad2046c 100644 --- a/src/mtconnect/device_model/composition.hpp +++ b/src/mtconnect/device_model/composition.hpp @@ -23,6 +23,8 @@ namespace mtconnect { namespace device_model { class Component; + + /// @brief Composition entity class AGENT_LIB_API Composition : public entity::Entity { public: @@ -30,6 +32,8 @@ namespace mtconnect { static entity::FactoryPtr getFactory(); static entity::FactoryPtr getRoot(); + /// @brief create a topic name from the component + /// @return topic name const std::string getTopicName() const { using namespace std; @@ -48,8 +52,14 @@ namespace mtconnect { return *m_topicName; } + /// @brief component this composition is a part of + /// @param[in] component the component void setComponent(std::shared_ptr component) { m_component = component; } + /// @brief get the component this is a part of + /// @return shared pointer to the component std::shared_ptr getComponent() const { return m_component.lock(); } + /// @brief cover method for getComponent + /// @return shared pointer to the component auto getParent() const { return getComponent(); } protected: diff --git a/src/mtconnect/device_model/configuration/configuration.hpp b/src/mtconnect/device_model/configuration/configuration.hpp index bcce229d..f9f70739 100644 --- a/src/mtconnect/device_model/configuration/configuration.hpp +++ b/src/mtconnect/device_model/configuration/configuration.hpp @@ -21,17 +21,14 @@ #include "mtconnect/entity/entity.hpp" #include "mtconnect/entity/factory.hpp" -namespace mtconnect { - namespace device_model { - namespace configuration { - struct Configuration - { - static entity::FactoryPtr getFactory(); - static entity::FactoryPtr getRoot(); - }; +/// @brief Configuration namespace +namespace mtconnect::device_model::configuration { + /// @brief Configuration Entity + struct Configuration + { + static entity::FactoryPtr getFactory(); + static entity::FactoryPtr getRoot(); + }; - // TODO: Need to support extended configuration - - } // namespace configuration - } // namespace device_model -} // namespace mtconnect + // TODO: Need to support extended configuration +} // namespace mtconnect::device_model::configuration diff --git a/src/mtconnect/device_model/configuration/coordinate_systems.hpp b/src/mtconnect/device_model/configuration/coordinate_systems.hpp index fe57bd94..ea414c33 100644 --- a/src/mtconnect/device_model/configuration/coordinate_systems.hpp +++ b/src/mtconnect/device_model/configuration/coordinate_systems.hpp @@ -20,14 +20,10 @@ #include "configuration.hpp" #include "mtconnect/config.hpp" -namespace mtconnect { - using namespace entity; - namespace device_model { - namespace configuration { - struct CoordinateSystems - { - static FactoryPtr getFactory(); - }; - } // namespace configuration - } // namespace device_model -} // namespace mtconnect +namespace mtconnect::device_model::configuration { + /// @brief Coordinate Systems container + struct CoordinateSystems + { + static entity::FactoryPtr getFactory(); + }; +} // namespace mtconnect::device_model::configuration diff --git a/src/mtconnect/device_model/configuration/motion.hpp b/src/mtconnect/device_model/configuration/motion.hpp index 4bd3e252..ec73c9c2 100644 --- a/src/mtconnect/device_model/configuration/motion.hpp +++ b/src/mtconnect/device_model/configuration/motion.hpp @@ -20,15 +20,10 @@ #include "configuration.hpp" #include "mtconnect/config.hpp" -namespace mtconnect { - using namespace entity; - namespace device_model { - namespace configuration { - struct Motion - { - static FactoryPtr getFactory(); - }; - } // namespace configuration - } // namespace device_model - -} // namespace mtconnect +namespace mtconnect::device_model::configuration { + /// @brief Motion definition for the component + struct Motion + { + static entity::FactoryPtr getFactory(); + }; +} // namespace mtconnect::device_model::configuration diff --git a/src/mtconnect/device_model/configuration/relationships.hpp b/src/mtconnect/device_model/configuration/relationships.hpp index bd36349b..62eeb75a 100644 --- a/src/mtconnect/device_model/configuration/relationships.hpp +++ b/src/mtconnect/device_model/configuration/relationships.hpp @@ -20,14 +20,10 @@ #include "configuration.hpp" #include "mtconnect/config.hpp" -namespace mtconnect { - using namespace entity; - namespace device_model { - namespace configuration { - struct Relationships - { - static FactoryPtr getFactory(); - }; - } // namespace configuration - } // namespace device_model -} // namespace mtconnect +namespace mtconnect::device_model::configuration { + /// @brief Component relationships to other components, assets, and devices + struct Relationships + { + static entity::FactoryPtr getFactory(); + }; +} // namespace mtconnect::device_model::configuration diff --git a/src/mtconnect/device_model/configuration/sensor_configuration.hpp b/src/mtconnect/device_model/configuration/sensor_configuration.hpp index ec026b90..8a4c54b5 100644 --- a/src/mtconnect/device_model/configuration/sensor_configuration.hpp +++ b/src/mtconnect/device_model/configuration/sensor_configuration.hpp @@ -20,15 +20,10 @@ #include "configuration.hpp" #include "mtconnect/config.hpp" -namespace mtconnect { - using namespace entity; - namespace device_model { - namespace configuration { - struct SensorConfiguration - { - static FactoryPtr getFactory(); - }; - } // namespace configuration - } // namespace device_model - -} // namespace mtconnect +namespace mtconnect::device_model::configuration { + /// @brief Sensor Configuration for the component + struct SensorConfiguration + { + static entity::FactoryPtr getFactory(); + }; +} // namespace mtconnect::device_model::configuration diff --git a/src/mtconnect/device_model/configuration/solid_model.hpp b/src/mtconnect/device_model/configuration/solid_model.hpp index b810b60f..71d60bcf 100644 --- a/src/mtconnect/device_model/configuration/solid_model.hpp +++ b/src/mtconnect/device_model/configuration/solid_model.hpp @@ -20,14 +20,10 @@ #include "configuration.hpp" #include "mtconnect/config.hpp" -namespace mtconnect { - using namespace entity; - namespace device_model { - namespace configuration { - struct SolidModel - { - static FactoryPtr getFactory(); - }; - } // namespace configuration - } // namespace device_model -} // namespace mtconnect +namespace mtconnect::device_model::configuration { + /// @brief Solid Model/3D Geometry for the component + struct SolidModel + { + static entity::FactoryPtr getFactory(); + }; +} // namespace mtconnect::device_model::configuration diff --git a/src/mtconnect/device_model/configuration/specifications.hpp b/src/mtconnect/device_model/configuration/specifications.hpp index c4dd2b0c..90da6bc3 100644 --- a/src/mtconnect/device_model/configuration/specifications.hpp +++ b/src/mtconnect/device_model/configuration/specifications.hpp @@ -20,14 +20,10 @@ #include "configuration.hpp" #include "mtconnect/config.hpp" -namespace mtconnect { - using namespace entity; - namespace device_model { - namespace configuration { - struct Specifications - { - static FactoryPtr getFactory(); - }; - } // namespace configuration - } // namespace device_model -} // namespace mtconnect +namespace mtconnect::device_model::configuration { + /// @brief Specifications and constraints for the component + struct Specifications + { + static entity::FactoryPtr getFactory(); + }; +} // namespace mtconnect::device_model::configuration diff --git a/src/mtconnect/device_model/data_item/constraints.hpp b/src/mtconnect/device_model/data_item/constraints.hpp index 2c8f328b..70f6c38f 100644 --- a/src/mtconnect/device_model/data_item/constraints.hpp +++ b/src/mtconnect/device_model/data_item/constraints.hpp @@ -22,32 +22,29 @@ #include "mtconnect/entity/entity.hpp" #include "mtconnect/entity/factory.hpp" -namespace mtconnect { - namespace device_model { - namespace data_item { - class AGENT_LIB_API Constraints : public entity::Entity +namespace mtconnect::device_model::data_item { + /// @brief Constraint for a data item + class AGENT_LIB_API Constraints : public entity::Entity + { + public: + static entity::FactoryPtr getFactory() + { + using namespace mtconnect::entity; + static FactoryPtr factory; + if (!factory) { - public: - static entity::FactoryPtr getFactory() - { - using namespace mtconnect::entity; - static FactoryPtr factory; - if (!factory) - { - auto limit = std::make_shared(Requirements {{"VALUE", DOUBLE, true}}); - auto value = std::make_shared(Requirements {{"VALUE", true}}); - auto filter = Filter::getFactory()->factoryFor("Filter")->deepCopy(); - filter->getRequirement("type")->setMultiplicity(0, 1); - factory = std::make_shared( - Requirements {{"Minimum", ENTITY, limit, 0, 1}, - {"Maximum", ENTITY, limit, 0, 1}, - {"Nominal", ENTITY, limit, 0, 1}, - {"Value", ENTITY, value, 0, Requirement::Infinite}, - {"Filter", ENTITY, filter, 0, Requirement::Infinite}}); - } - return factory; - } - }; - } // namespace data_item - } // namespace device_model -} // namespace mtconnect + auto limit = std::make_shared(Requirements {{"VALUE", DOUBLE, true}}); + auto value = std::make_shared(Requirements {{"VALUE", true}}); + auto filter = Filter::getFactory()->factoryFor("Filter")->deepCopy(); + filter->getRequirement("type")->setMultiplicity(0, 1); + factory = std::make_shared( + Requirements {{"Minimum", ENTITY, limit, 0, 1}, + {"Maximum", ENTITY, limit, 0, 1}, + {"Nominal", ENTITY, limit, 0, 1}, + {"Value", ENTITY, value, 0, Requirement::Infinite}, + {"Filter", ENTITY, filter, 0, Requirement::Infinite}}); + } + return factory; + } + }; +} // namespace mtconnect::device_model::data_item diff --git a/src/mtconnect/device_model/data_item/data_item.cpp b/src/mtconnect/device_model/data_item/data_item.cpp index b4df08de..89842b47 100644 --- a/src/mtconnect/device_model/data_item/data_item.cpp +++ b/src/mtconnect/device_model/data_item/data_item.cpp @@ -117,11 +117,11 @@ namespace mtconnect { m_observationName.setQName(obs, pre); - const static unordered_map reps = {{"VALUE", VALUE}, - {"TIME_SERIES", TIME_SERIES}, - {"DISCRETE", DISCRETE}, - {"DATA_SET", DATA_SET}, - {"TABLE", TABLE}}; + const static unordered_map reps = {{"VALUE", VALUE}, + {"TIME_SERIES", TIME_SERIES}, + {"DISCRETE", DISCRETE}, + {"DATA_SET", DATA_SET}, + {"TABLE", TABLE}}; auto rep = maybeGet("representation"); if (rep) m_representation = reps.find(*rep)->second; diff --git a/src/mtconnect/device_model/data_item/data_item.hpp b/src/mtconnect/device_model/data_item/data_item.hpp index fe7991a9..826e1634 100644 --- a/src/mtconnect/device_model/data_item/data_item.hpp +++ b/src/mtconnect/device_model/data_item/data_item.hpp @@ -41,19 +41,22 @@ namespace mtconnect { namespace device_model { class Composition; + /// @brief DataItem related entities namespace data_item { + /// @brief Data Item entity class AGENT_LIB_API DataItem : public entity::Entity, public observation::ChangeSignaler { public: - // Enumeration for data item category - enum ECategory + /// @brief MTConnect DataItem Enumeration for category + enum Category { SAMPLE, EVENT, CONDITION }; - enum ERepresentation + /// @brief MTConnect DataItem Enumeration for representation + enum Representation { VALUE, TIME_SERIES, @@ -62,6 +65,7 @@ namespace mtconnect { TABLE }; + /// @brief MTConnect DataItem classifications enum SpecialClass { CONDITION_CLS, @@ -74,10 +78,17 @@ namespace mtconnect { }; public: - // Construct a data item with appropriate attributes mapping + /// @brief constructor for a data item. name is always `DataItem`. + /// + /// @note Do not use this method directly. Use the `make()` method. DataItem(const std::string &name, const entity::Properties &props); static entity::FactoryPtr getFactory(); static entity::FactoryPtr getRoot(); + + /// @brief make method to create + /// @param[in] props data item properties + /// @param[in,out] errors list of errors creating the data item + /// @return shared pointer to DataItem static std::shared_ptr make(const entity::Properties &props, entity::ErrorList &errors) { @@ -89,31 +100,52 @@ namespace mtconnect { // Destructor ~DataItem() override = default; - // Getter methods for data item specs + /// @name Cached transformed and derived property access methods + ///@{ const auto &getId() const { return m_id; } const auto &getName() const { return m_name; } const auto &getSource() const { return get("Source"); } + + /// @brief get the name or the id of the data item + /// @return the preferred name const auto &getPreferredName() const { return m_preferredName; } - const auto &getObservationName() const { return m_observationName; } - const auto &getObservationProperties() const { return m_observatonProperties; } const auto &getMinimumDelta() const { return m_minimumDelta; } const auto &getMinimumPeriod() const { return m_minimumPeriod; } - bool hasName(const std::string &name) const; + /// @brief get a key related to the data item for creating observations + /// @return a key const auto &getKey() const { return m_key; } - const auto &getType() { return get("type"); } const auto &getSubType() { return get("subType"); } + + /// @brief get the pascalized name for the data item when represented as a observation + /// @return observation name + const auto &getObservationName() const { return m_observationName; } + /// @brief get the properties to build an observation + /// @return observation properties + const auto &getObservationProperties() const { return m_observatonProperties; } + + /// @brief get the topic with the path + /// @return data item topic const auto &getTopic() const { return m_topic; } + /// @brief get the topic name leaf node for this data item + /// @return the topic name const auto &getTopicName() const { return m_topicName; } - ECategory getCategory() const { return m_category; } - ERepresentation getRepresentation() const { return m_representation; } + Category getCategory() const { return m_category; } + Representation getRepresentation() const { return m_representation; } SpecialClass getSpecialClass() const { return m_specialClass; } const auto &getConstantValue() const { return m_constantValue; } + ///@} + + /// @brief make this data item a constant + /// @param[in] value constant value void setConstantValue(const std::string &value); - // Returns true if data item is a sample + /// @name boolean methods to interigate the data item + ///@{ + + bool hasName(const std::string &name) const; bool isSample() const { return m_category == SAMPLE; } bool isEvent() const { return m_category == EVENT; } bool isCondition() const { return m_category == CONDITION; } @@ -128,6 +160,7 @@ namespace mtconnect { bool isDiscrete() const { return m_discrete; } bool isThreeSpace() const { return m_specialClass == THREE_SPACE_CLS; } bool isOrphan() const { return m_component.expired(); } + ///@} void makeDiscrete() { @@ -135,6 +168,7 @@ namespace mtconnect { m_discrete = true; } + /// @brief Build the topic void makeTopic(); // Value converter @@ -144,7 +178,8 @@ namespace mtconnect { m_converter = std::make_unique(conv); } - // Set/get component that data item is associated with + /// @brief Set the associated component + /// @param[in] the component void setComponent(ComponentPtr component) { m_component = component; @@ -154,14 +189,25 @@ namespace mtconnect { m_composition = component->getComposition(*cid); } } + /// @brief get the associated component + /// @return shared pointer to the component ComponentPtr getComponent() const { return m_component.lock(); } + /// @brief get the associated composition + /// @return shared pointer to the composition CompositionPtr getComposition() const { return m_composition.lock(); } - // Get the name for the adapter feed + /// @brief get the preferred name + /// @return the preferred name const std::string &getSourceOrName() const { return m_preferredName; } + /// @brief get the source + /// @return source if available const std::optional &getDataSource() const { return m_dataSource; } + /// @brief set the data source + /// @param[in] source the source void setDataSource(const std::string &source) { m_dataSource = source; } + /// @brief set the topic for the data item + /// @param[in] topic the topic void setTopic(const std::string &topic) { m_topic = topic; } bool operator<(const DataItem &another) const; @@ -189,7 +235,7 @@ namespace mtconnect { std::string m_topicName; // Category of data item - ECategory m_category; + Category m_category; const char *m_categoryText; // Type for observation @@ -197,7 +243,7 @@ namespace mtconnect { entity::Properties m_observatonProperties; // Representation of data item - ERepresentation m_representation {VALUE}; + Representation m_representation {VALUE}; SpecialClass m_specialClass {NONE_CLS}; bool m_discrete; diff --git a/src/mtconnect/device_model/data_item/definition.hpp b/src/mtconnect/device_model/data_item/definition.hpp index 50cded52..02d1d594 100644 --- a/src/mtconnect/device_model/data_item/definition.hpp +++ b/src/mtconnect/device_model/data_item/definition.hpp @@ -22,73 +22,71 @@ #include "mtconnect/config.hpp" #include "mtconnect/entity/entity.hpp" -namespace mtconnect { - namespace device_model { - namespace data_item { - class AGENT_LIB_API Definition : public entity::Entity +namespace mtconnect::device_model::data_item { + /// @brief Definition of a data item + class AGENT_LIB_API Definition : public entity::Entity + { + public: + /// @brief Entry in a data item defintion + class AGENT_LIB_API Entry : public entity::Entity + { + public: + using entity::Entity::Entity; + const entity::Value &getIdentity() const override { - public: - class AGENT_LIB_API Entry : public entity::Entity - { - public: - using entity::Entity::Entity; - const entity::Value &getIdentity() const override - { - auto it = m_properties.find("key"); - if (it == m_properties.end()) - return getProperty("keyType"); - else - return it->second; - } - }; + auto it = m_properties.find("key"); + if (it == m_properties.end()) + return getProperty("keyType"); + else + return it->second; + } + }; - static entity::FactoryPtr getFactory() - { - using namespace mtconnect::entity; - using namespace std; - static FactoryPtr definition; - if (!definition) - { - auto cell = make_shared(Requirements {{"Description", false}, - {"key", false}, - {"keyType", false}, - {"type", false}, - {"subType", false}, - {"units", false}}); + static entity::FactoryPtr getFactory() + { + using namespace mtconnect::entity; + using namespace std; + static FactoryPtr definition; + if (!definition) + { + auto cell = make_shared(Requirements {{"Description", false}, + {"key", false}, + {"keyType", false}, + {"type", false}, + {"subType", false}, + {"units", false}}); - auto cells = make_shared( - Requirements {{"CellDefinition", ENTITY, cell, 1, Requirement::Infinite}}); - cells->setFunction([](const std::string &name, Properties &props) -> EntityPtr { - auto ptr = make_shared(name, props); - return dynamic_pointer_cast(ptr); - }); + auto cells = make_shared( + Requirements {{"CellDefinition", ENTITY, cell, 1, Requirement::Infinite}}); + cells->setFunction([](const std::string &name, Properties &props) -> EntityPtr { + auto ptr = make_shared(name, props); + return dynamic_pointer_cast(ptr); + }); - auto entry = - make_shared(Requirements {{"Description", false}, - {"key", false}, - {"keyType", false}, - {"type", false}, - {"subType", false}, - {"units", false}, - {"CellDefinitions", ENTITY_LIST, cells, false}}); - entry->setOrder({"Description", "CellDefinitions"}); - entry->setFunction([](const std::string &name, Properties &props) -> EntityPtr { - auto ptr = make_shared(name, props); - return dynamic_pointer_cast(ptr); - }); + auto entry = + make_shared(Requirements {{"Description", false}, + {"key", false}, + {"keyType", false}, + {"type", false}, + {"subType", false}, + {"units", false}, + {"CellDefinitions", ENTITY_LIST, cells, false}}); + entry->setOrder({"Description", "CellDefinitions"}); + entry->setFunction([](const std::string &name, Properties &props) -> EntityPtr { + auto ptr = make_shared(name, props); + return dynamic_pointer_cast(ptr); + }); - auto entries = make_shared( - Requirements {{"EntryDefinition", ENTITY, entry, 1, Requirement::Infinite}}); - definition = make_shared( - Requirements {{"Description", false}, - {"EntryDefinitions", ENTITY_LIST, entries, false}, - {"CellDefinitions", ENTITY_LIST, cells, false}}); - definition->setOrder({"Description", "EntryDefinitions", "CellDefinitions"}); - } + auto entries = make_shared( + Requirements {{"EntryDefinition", ENTITY, entry, 1, Requirement::Infinite}}); + definition = + make_shared(Requirements {{"Description", false}, + {"EntryDefinitions", ENTITY_LIST, entries, false}, + {"CellDefinitions", ENTITY_LIST, cells, false}}); + definition->setOrder({"Description", "EntryDefinitions", "CellDefinitions"}); + } - return definition; - } - }; - } // namespace data_item - } // namespace device_model -} // namespace mtconnect + return definition; + } + }; +} // namespace mtconnect::device_model::data_item diff --git a/src/mtconnect/device_model/data_item/filter.hpp b/src/mtconnect/device_model/data_item/filter.hpp index 50d93720..0842770b 100644 --- a/src/mtconnect/device_model/data_item/filter.hpp +++ b/src/mtconnect/device_model/data_item/filter.hpp @@ -21,23 +21,20 @@ #include "mtconnect/entity/entity.hpp" #include "mtconnect/entity/factory.hpp" -namespace mtconnect { - namespace device_model { - namespace data_item { - class AGENT_LIB_API Filter : public entity::Entity - { - public: - static entity::FactoryPtr getFactory() - { - using namespace mtconnect::entity; - using namespace std; - static auto filter = make_shared(Requirements { - {"type", ControlledVocab {"PERIOD", "MINIMUM_DELTA"}}, {"VALUE", DOUBLE, true}}); - static auto filters = make_shared( - Requirements {{"Filter", ENTITY, filter, 1, Requirement::Infinite}}); - return filters; - } - }; - } // namespace data_item - } // namespace device_model -} // namespace mtconnect +namespace mtconnect::device_model::data_item { + /// @brief Data Item Filter + class AGENT_LIB_API Filter : public entity::Entity + { + public: + static entity::FactoryPtr getFactory() + { + using namespace mtconnect::entity; + using namespace std; + static auto filter = make_shared(Requirements { + {"type", ControlledVocab {"PERIOD", "MINIMUM_DELTA"}}, {"VALUE", DOUBLE, true}}); + static auto filters = + make_shared(Requirements {{"Filter", ENTITY, filter, 1, Requirement::Infinite}}); + return filters; + } + }; +} // namespace mtconnect::device_model::data_item diff --git a/src/mtconnect/device_model/data_item/relationships.hpp b/src/mtconnect/device_model/data_item/relationships.hpp index e78e10ad..a141bf58 100644 --- a/src/mtconnect/device_model/data_item/relationships.hpp +++ b/src/mtconnect/device_model/data_item/relationships.hpp @@ -22,84 +22,85 @@ #include "mtconnect/config.hpp" #include "mtconnect/entity/entity.hpp" -namespace mtconnect { - namespace device_model { - namespace data_item { - class DataItem; - class AGENT_LIB_API Relationship : public entity::Entity - { - public: - using entity::Entity::Entity; - ~Relationship() override = default; - - const entity::Value &getIdentity() const override { return getProperty("idRef"); } +namespace mtconnect::device_model::data_item { + class DataItem; - static entity::FactoryPtr getDataItemFactory() - { - using namespace mtconnect::entity; - using namespace std; + /// @brief Data Item Relationship to an attachment, coordinate system, limit, or observation. + class AGENT_LIB_API Relationship : public entity::Entity + { + public: + using entity::Entity::Entity; + ~Relationship() override = default; - static FactoryPtr factory; - if (!factory) - { - factory = make_shared(Requirements { - {"type", - ControlledVocab {"ATTACHMENT", "COORDINATE_SYSTEM", "LIMIT", "OBSERVATION"}, true}, - {"name", false}, - {"idRef", true}}); - factory->setFunction([](const std::string &name, Properties &props) -> EntityPtr { - return std::make_shared(name, props); - }); - } - return factory; - } + const entity::Value &getIdentity() const override { return getProperty("idRef"); } - static entity::FactoryPtr getSpecificationFactory() - { - using namespace mtconnect::entity; - using namespace std; + static entity::FactoryPtr getDataItemFactory() + { + using namespace mtconnect::entity; + using namespace std; - static FactoryPtr factory; - if (!factory) - { - factory = make_shared(Requirements { - {"type", ControlledVocab {"LIMIT"}, true}, {"name", false}, {"idRef", true}}); - factory->setFunction([](const std::string &name, Properties &props) -> EntityPtr { - return std::make_shared(name, props); - }); - } - return factory; - } + static FactoryPtr factory; + if (!factory) + { + factory = make_shared(Requirements { + {"type", ControlledVocab {"ATTACHMENT", "COORDINATE_SYSTEM", "LIMIT", "OBSERVATION"}, + true}, + {"name", false}, + {"idRef", true}}); + factory->setFunction([](const std::string &name, Properties &props) -> EntityPtr { + return std::make_shared(name, props); + }); + } + return factory; + } - std::weak_ptr m_target; - }; + static entity::FactoryPtr getSpecificationFactory() + { + using namespace mtconnect::entity; + using namespace std; - class AGENT_LIB_API Relationships : public entity::Entity + static FactoryPtr factory; + if (!factory) { - public: - using entity::Entity::Entity; - ~Relationships() override = default; + factory = make_shared(Requirements { + {"type", ControlledVocab {"LIMIT"}, true}, {"name", false}, {"idRef", true}}); + factory->setFunction([](const std::string &name, Properties &props) -> EntityPtr { + return std::make_shared(name, props); + }); + } + return factory; + } - static entity::FactoryPtr getFactory() - { - using namespace mtconnect::entity; - using namespace std; - static FactoryPtr relationships; - if (!relationships) - { - auto di = Relationship::getDataItemFactory(); - auto spec = Relationship::getSpecificationFactory(); - relationships = make_shared( - Requirements {{"SpecificationRelationship", ENTITY, spec, 0, Requirement::Infinite}, - {"DataItemRelationship", ENTITY, di, 0, Requirement::Infinite}}); - relationships->setMinListSize(1); - relationships->setFunction([](const std::string &name, Properties &props) -> EntityPtr { - return std::make_shared(name, props); - }); - } - return relationships; - } - }; - } // namespace data_item - } // namespace device_model -} // namespace mtconnect + std::weak_ptr m_target; + }; + + /// @brief Aggregator class for Data Item Relationships. Two types: + /// - SpecificationRelationship + /// - DataItemRelationship + class AGENT_LIB_API Relationships : public entity::Entity + { + public: + using entity::Entity::Entity; + ~Relationships() override = default; + + static entity::FactoryPtr getFactory() + { + using namespace mtconnect::entity; + using namespace std; + static FactoryPtr relationships; + if (!relationships) + { + auto di = Relationship::getDataItemFactory(); + auto spec = Relationship::getSpecificationFactory(); + relationships = make_shared( + Requirements {{"SpecificationRelationship", ENTITY, spec, 0, Requirement::Infinite}, + {"DataItemRelationship", ENTITY, di, 0, Requirement::Infinite}}); + relationships->setMinListSize(1); + relationships->setFunction([](const std::string &name, Properties &props) -> EntityPtr { + return std::make_shared(name, props); + }); + } + return relationships; + } + }; +} // namespace mtconnect::device_model::data_item diff --git a/src/mtconnect/device_model/data_item/source.hpp b/src/mtconnect/device_model/data_item/source.hpp index 35d549b4..561aaa38 100644 --- a/src/mtconnect/device_model/data_item/source.hpp +++ b/src/mtconnect/device_model/data_item/source.hpp @@ -21,6 +21,7 @@ #include "mtconnect/entity/entity.hpp" namespace mtconnect::device_model::data_item { + /// @brief Data item source class AGENT_LIB_API Source : public entity::Entity { public: diff --git a/src/mtconnect/device_model/data_item/unit_conversion.cpp b/src/mtconnect/device_model/data_item/unit_conversion.cpp index 782c3376..f1767c0d 100644 --- a/src/mtconnect/device_model/data_item/unit_conversion.cpp +++ b/src/mtconnect/device_model/data_item/unit_conversion.cpp @@ -19,172 +19,176 @@ using namespace std; -namespace mtconnect { - namespace device_model { - namespace data_item { - std::unordered_map UnitConversion::m_conversions( - {{"INCH-MILLIMETER", 25.4}, - {"FOOT-MILLIMETER", 304.8}, - {"CENTIMETER-MILLIMETER", 10.0}, - {"DECIMETER-MILLIMETER", 100.0}, - {"METER-MILLIMETER", 1000.0}, - {"FAHRENHEIT-CELSIUS", {(5.0 / 9.0), -32.0}}, - {"POUND-GRAM", 453.59237}, - {"GRAM-KILOGRAM", 1 / 1000.0}, - {"RADIAN-DEGREE", 57.2957795}, - {"SECOND-MINUTE", 1.0 / 60.0}, - {"MINUTE-SECOND", 60.0}, - {"POUND/INCH^2-PASCAL", 6894.76}, - {"HOUR-SECOND", 3600.0}}); - - std::unordered_set UnitConversion::m_mtconnectUnits( - {"AMPERE", - "CELSIUS", - "COUNT", - "DECIBEL", - "DEGREE", - "DEGREE_3D", - "DEGREE/SECOND", - "DEGREE/SECOND^2", - "HERTZ", - "JOULE", - "KILOGRAM", - "LITER", - "LITER/SECOND", - "MICRO_RADIAN", - "MILLIMETER", - "MILLIMETER_3D", - "MILLIMETER/REVOLUTION", - "MILLIMETER/SECOND", - "MILLIMETER/SECOND^2", - "NEWTON", - "NEWTON_METER", - "OHM", - "PASCAL", - "PASCAL_SECOND", - "PERCENT", - "PH", - "REVOLUTION/MINUTE", - "SECOND", - "SIEMENS/METER", - "VOLT", - "VOLT_AMPERE", - "VOLT_AMPERE_REACTIVE", - "WATT", - "WATT_SECOND", - "REVOLUTION/SECOND", - "REVOLUTION/SECOND^2", - "GRAM/CUBIC_METER", - "CUBIC_MILLIMETER", - "CUBIC_MILLIMETER/SECOND", - "CUBIC_MILLIMETER/SECOND^2", - "MILLIGRAM", - "MILLIGRAM/CUBIC_MILLIMETER", - "MILLILITER", - "COUNT/SECOND", - "PASCAL/SECOND", - "UNIT_VECTOR_3D"}); - - static pair scaleAndPower(string_view &unit) +namespace mtconnect::device_model::data_item { + /// @brief Unit conversions from-to + std::unordered_map UnitConversion::m_conversions( + {{"INCH-MILLIMETER", 25.4}, + {"FOOT-MILLIMETER", 304.8}, + {"CENTIMETER-MILLIMETER", 10.0}, + {"DECIMETER-MILLIMETER", 100.0}, + {"METER-MILLIMETER", 1000.0}, + {"FAHRENHEIT-CELSIUS", {(5.0 / 9.0), -32.0}}, + {"POUND-GRAM", 453.59237}, + {"GRAM-KILOGRAM", 1 / 1000.0}, + {"RADIAN-DEGREE", 57.2957795}, + {"SECOND-MINUTE", 1.0 / 60.0}, + {"MINUTE-SECOND", 60.0}, + {"POUND/INCH^2-PASCAL", 6894.76}, + {"HOUR-SECOND", 3600.0}}); + + /// @brief Known MTConnect units + std::unordered_set UnitConversion::m_mtconnectUnits({"AMPERE", + "CELSIUS", + "COUNT", + "DECIBEL", + "DEGREE", + "DEGREE_3D", + "DEGREE/SECOND", + "DEGREE/SECOND^2", + "HERTZ", + "JOULE", + "KILOGRAM", + "LITER", + "LITER/SECOND", + "MICRO_RADIAN", + "MILLIMETER", + "MILLIMETER_3D", + "MILLIMETER/REVOLUTION", + "MILLIMETER/SECOND", + "MILLIMETER/SECOND^2", + "NEWTON", + "NEWTON_METER", + "OHM", + "PASCAL", + "PASCAL_SECOND", + "PERCENT", + "PH", + "REVOLUTION/MINUTE", + "SECOND", + "SIEMENS/METER", + "VOLT", + "VOLT_AMPERE", + "VOLT_AMPERE_REACTIVE", + "WATT", + "WATT_SECOND", + "REVOLUTION/SECOND", + "REVOLUTION/SECOND^2", + "GRAM/CUBIC_METER", + "CUBIC_MILLIMETER", + "CUBIC_MILLIMETER/SECOND", + "CUBIC_MILLIMETER/SECOND^2", + "MILLIGRAM", + "MILLIGRAM/CUBIC_MILLIMETER", + "MILLILITER", + "COUNT/SECOND", + "PASCAL/SECOND", + "UNIT_VECTOR_3D"}); + + /// @brief Handle KILO and CUBIC prefixes to provide the correct scaling + /// @param[in] unit the incoming unit + /// @return the {scale,power} as a pair. + static pair scaleAndPower(string_view &unit) + { + double power = 1.0, scale = 1.0; + + if (unit.compare(0, 4, "KILO") == 0) + { + scale = 1000; + unit.remove_prefix(4); + } + else if (unit.compare(0, 6, "CUBIC_") == 0) + { + unit.remove_prefix(6); + power = 3.0; + } + else if (auto p = unit.find('^'); p != string_view::npos) + { + power = stod(string(unit.substr(p + 1))); + unit.remove_suffix(unit.length() - p); + } + + return {scale, power}; + } + + /// @brief Create a unit conversion + /// @param[in] from units from + /// @param[in] to units to + /// @return A units conversion object + std::unique_ptr UnitConversion::make(const std::string &from, + const std::string &to) + { + if (from == to) + return nullptr; + + string key(from); + key = key.append("-").append(to); + + const auto &conversion = m_conversions.find(string(key)); + if (conversion != m_conversions.end()) + return make_unique(conversion->second); + + double factor {1.0}, offset {0.0}; + std::string_view source(from); + std::string_view target(to); + + // Always convert back to MTConnect Units. + auto t3D = target.rfind("_3D"); + auto s3D = source.rfind("_3D"); + if (t3D != string_view::npos && s3D != string_view::npos) + { + source.remove_suffix(3); + target.remove_suffix(3); + } + else if (t3D != string_view::npos || s3D != string_view::npos) + return nullptr; + + auto sslash = source.find('/'); + auto tslash = target.find('/'); + if (sslash == string_view::npos && tslash == string_view::npos) + { + auto [sscale, spower] = scaleAndPower(source); + auto [tscale, tpower] = scaleAndPower(target); + + if (spower != tpower) + return nullptr; + + factor = sscale / tscale; + + key = source; + key = key.append("-").append(target); + + const auto &conversion = m_conversions.find(string(key)); + // Check for no support units and not power or factor scaling. + if (conversion == m_conversions.end() && factor == 1.0) + return nullptr; + else if (conversion != m_conversions.end()) { - double power = 1.0, scale = 1.0; - - if (unit.compare(0, 4, "KILO") == 0) - { - scale = 1000; - unit.remove_prefix(4); - } - else if (unit.compare(0, 6, "CUBIC_") == 0) - { - unit.remove_prefix(6); - power = 3.0; - } - else if (auto p = unit.find('^'); p != string_view::npos) - { - power = stod(string(unit.substr(p + 1))); - unit.remove_suffix(unit.length() - p); - } - - return {scale, power}; + factor *= conversion->second.factor(); + offset = conversion->second.offset(); } - std::unique_ptr UnitConversion::make(const std::string &from, - const std::string &to) - { - if (from == to) - return nullptr; - - string key(from); - key = key.append("-").append(to); - - const auto &conversion = m_conversions.find(string(key)); - if (conversion != m_conversions.end()) - return make_unique(conversion->second); - - double factor {1.0}, offset {0.0}; - std::string_view source(from); - std::string_view target(to); - - // Always convert back to MTConnect Units. - auto t3D = target.rfind("_3D"); - auto s3D = source.rfind("_3D"); - if (t3D != string_view::npos && s3D != string_view::npos) - { - source.remove_suffix(3); - target.remove_suffix(3); - } - else if (t3D != string_view::npos || s3D != string_view::npos) - return nullptr; - - auto sslash = source.find('/'); - auto tslash = target.find('/'); - if (sslash == string_view::npos && tslash == string_view::npos) - { - auto [sscale, spower] = scaleAndPower(source); - auto [tscale, tpower] = scaleAndPower(target); - - if (spower != tpower) - return nullptr; - - factor = sscale / tscale; - - key = source; - key = key.append("-").append(target); - - const auto &conversion = m_conversions.find(string(key)); - // Check for no support units and not power or factor scaling. - if (conversion == m_conversions.end() && factor == 1.0) - return nullptr; - else if (conversion != m_conversions.end()) - { - factor *= conversion->second.factor(); - offset = conversion->second.offset(); - } - - if (tpower != 1.0) - factor = pow(factor, tpower); - } - else if (sslash == string_view::npos || tslash == string_view::npos) - { - return nullptr; - } - else - { - string_view snumerator(source.substr(0, sslash)); - string_view tnumerator(target.substr(0, tslash)); - string_view sdenominator(source.substr(sslash + 1)); - string_view tdenominator(target.substr(tslash + 1)); - - auto num = make(string(snumerator), string(tnumerator)); - auto den = make(string(sdenominator), string(tdenominator)); - auto n = num ? num->factor() : 1.0; - auto d = den ? den->factor() : 1.0; - - factor = n / d; - } - - return make_unique(factor, offset); - } - } // namespace data_item - } // namespace device_model -} // namespace mtconnect + if (tpower != 1.0) + factor = pow(factor, tpower); + } + else if (sslash == string_view::npos || tslash == string_view::npos) + { + return nullptr; + } + else + { + string_view snumerator(source.substr(0, sslash)); + string_view tnumerator(target.substr(0, tslash)); + string_view sdenominator(source.substr(sslash + 1)); + string_view tdenominator(target.substr(tslash + 1)); + + auto num = make(string(snumerator), string(tnumerator)); + auto den = make(string(sdenominator), string(tdenominator)); + auto n = num ? num->factor() : 1.0; + auto d = den ? den->factor() : 1.0; + + factor = n / d; + } + + return make_unique(factor, offset); + } +} // namespace mtconnect::device_model::data_item diff --git a/src/mtconnect/device_model/data_item/unit_conversion.hpp b/src/mtconnect/device_model/data_item/unit_conversion.hpp index cc3baa17..c20fd515 100644 --- a/src/mtconnect/device_model/data_item/unit_conversion.hpp +++ b/src/mtconnect/device_model/data_item/unit_conversion.hpp @@ -25,63 +25,85 @@ #include "mtconnect/config.hpp" #include "mtconnect/entity/requirement.hpp" -namespace mtconnect { - namespace device_model { - namespace data_item { - class AGENT_LIB_API UnitConversion - { - public: - UnitConversion(double factor = 1.0, double offset = 0.0) - : m_factor(factor), m_offset(offset) - {} - UnitConversion(const UnitConversion &) = default; - ~UnitConversion() = default; +namespace mtconnect::device_model::data_item { + /// @brief Utility class to convert units from native to MTConnect + class AGENT_LIB_API UnitConversion + { + public: + /// @brief Create a unit conversion with a scalar factor and an offset + /// @param[in] factor + /// @param[in] offset + UnitConversion(double factor = 1.0, double offset = 0.0) : m_factor(factor), m_offset(offset) {} + UnitConversion(const UnitConversion &) = default; + ~UnitConversion() = default; - double convert(double value) const { return (value + m_offset) * m_factor; } + /// @brief convert a value + /// @param[in] value the value + /// @return converted value + double convert(double value) const { return (value + m_offset) * m_factor; } - entity::Vector convert(const entity::Vector &value) const - { - entity::Vector res(value.size()); - for (size_t i = 0; i < value.size(); i++) - res[i] = convert(value[i]); + /// @brief convert a vector of values + /// @param[in] value the vector of double + /// @return converted vector of doubles + entity::Vector convert(const entity::Vector &value) const + { + entity::Vector res(value.size()); + for (size_t i = 0; i < value.size(); i++) + res[i] = convert(value[i]); - return res; - } - void convert(entity::Vector &value) const - { - for (size_t i = 0; i < value.size(); i++) - value[i] = convert(value[i]); - } - entity::Value convertValue(const entity::Value &value) - { - if (const auto &v = std::get_if(&value)) - return {convert(*v)}; - else if (const auto &a = std::get_if(&value)) - return {convert(*a)}; - else - return nullptr; - } - void convertValue(entity::Value &value) - { - if (const auto &v = std::get_if(&value)) - value = convert(*v); - else if (const auto &a = std::get_if(&value)) - convert(*a); - } - void scale(double scale) { m_factor *= scale; } + return res; + } - static std::unique_ptr make(const std::string &from, const std::string &to); + /// @brief convert a vector of values in place + /// @param[in,out] value the vector of double + void convert(entity::Vector &value) const + { + for (size_t i = 0; i < value.size(); i++) + value[i] = convert(value[i]); + } + /// @brief Convert a entity variant Value if it holds a double or a vector of doubles + /// @param[in] value a Value variant + /// @return the converted value + entity::Value convertValue(const entity::Value &value) + { + if (const auto &v = std::get_if(&value)) + return {convert(*v)}; + else if (const auto &a = std::get_if(&value)) + return {convert(*a)}; + else + return nullptr; + } - double factor() const { return m_factor; } - double offset() const { return m_offset; } + /// @brief Convert a entity variant Value if it holds a double or a vector of doubles in place + /// @param[in,out] value a Value variant + void convertValue(entity::Value &value) + { + if (const auto &v = std::get_if(&value)) + value = convert(*v); + else if (const auto &a = std::get_if(&value)) + convert(*a); + } + /// @brief add a scaling factor to the conversion + void scale(double scale) { m_factor *= scale; } - protected: - double m_factor; - double m_offset; + /// @brief Create a unit conversion + /// @param[in] from units from + /// @param[in] to units to + /// @return A units conversion object + static std::unique_ptr make(const std::string &from, const std::string &to); - static std::unordered_map m_conversions; - static std::unordered_set m_mtconnectUnits; - }; - } // namespace data_item - } // namespace device_model -} // namespace mtconnect + /// @brief get the factor + /// @return the scaling factor + double factor() const { return m_factor; } + /// @brief get the offset + /// @return the offset + double offset() const { return m_offset; } + + protected: + double m_factor; + double m_offset; + + static std::unordered_map m_conversions; + static std::unordered_set m_mtconnectUnits; + }; +} // namespace mtconnect::device_model::data_item diff --git a/src/mtconnect/device_model/description.hpp b/src/mtconnect/device_model/description.hpp index bbaf8da9..c6c5059d 100644 --- a/src/mtconnect/device_model/description.hpp +++ b/src/mtconnect/device_model/description.hpp @@ -30,6 +30,7 @@ namespace mtconnect { namespace device_model { + /// @brief Component Description Entity struct Description { static entity::FactoryPtr getFactory(); diff --git a/src/mtconnect/device_model/device.hpp b/src/mtconnect/device_model/device.hpp index 078a0117..72b42c19 100644 --- a/src/mtconnect/device_model/device.hpp +++ b/src/mtconnect/device_model/device.hpp @@ -41,25 +41,34 @@ namespace mtconnect { class Adapter; } + /// @brief Namespace for all Device Model Related entities namespace device_model { + /// @brief Compenent entity representing a `Device` class AGENT_LIB_API Device : public Component { public: + /// @brief multi-index tag: Data items indexed by name struct ByName {}; + /// @brief multi-index tag: Data items indexed by id struct ById {}; + /// @brief multi-index tag: Data items index by Source struct BySource {}; + /// @brief multi-index tag: Data items indexed by type struct ByType {}; + /// @brief multi-index data item id extractor struct ExtractId { using result_type = std::string; const result_type &operator()(const WeakDataItemPtr d) const { return d.lock()->getId(); } }; - + /// @brief multi-index data item name extractor + /// + /// falls back to id if name is not given struct ExtractName { using result_type = std::string; @@ -79,7 +88,9 @@ namespace mtconnect { } } }; - + /// @brief multi-index data item source extractor + /// + /// falls back to id if name is not given struct ExtractSource { using result_type = std::string; @@ -98,7 +109,7 @@ namespace mtconnect { } } }; - + /// @brief multi-index data item type extractor struct ExtractType { using result_type = std::string; @@ -115,19 +126,23 @@ namespace mtconnect { } }; - // Mapping of device names to data items + /// @brief Mapping of device names to data items using DataItemIndex = mic::multi_index_container< WeakDataItemPtr, mic::indexed_by, ExtractId>, mic::hashed_unique, ExtractName>, mic::hashed_unique, ExtractSource>, mic::ordered_non_unique, ExtractType>>>; - // Constructor that sets variables from an attribute map + /// @brief Constructor that sets variables from an attribute map + /// @param[in] name the name of the device + /// @param[in] props the device properties Device(const std::string &name, entity::Properties &props); ~Device() override = default; + /// @brief get a shared pointer to the device auto getptr() const { return std::dynamic_pointer_cast(Entity::getptr()); } + /// @brief Indexes all the entities in this device void initialize() override { m_dataItems.clear(); @@ -141,13 +156,25 @@ namespace mtconnect { static entity::FactoryPtr getFactory(); static entity::FactoryPtr getRoot(); + /// @brief set any configuration options related to this device + /// @param[in] options the options + /// - `PreserveUUID` can be set to lock the uuid of this device void setOptions(const ConfigOptions &options); - // Add/get items to/from the device name to data item mapping + /// @brief Add a data item to the device + /// @param[in] dataItem shared pointer to the data item void addDeviceDataItem(DataItemPtr dataItem); + /// @brief get a data item by source, name, and id + /// @param[in] name the source, name, or id of the data item + /// @return shared pointer to the data item if found DataItemPtr getDeviceDataItem(const std::string &name) const; + /// @brief associate an adapter with the device + /// @param[in] anAdapter an adapter void addAdapter(source::adapter::Adapter *anAdapter) { m_adapters.emplace_back(anAdapter); } + /// @brief get a component by id + /// @param[in] aId the component id + /// @return shared pointer to the component if found ComponentPtr getComponentById(const std::string &aId) const { auto comp = m_componentsById.find(aId); @@ -156,35 +183,56 @@ namespace mtconnect { else return nullptr; } - void addComponent(ComponentPtr aComponent) + /// @brief Add a component to the device + /// @param[in] component the component + void addComponent(ComponentPtr component) { - m_componentsById.insert(make_pair(aComponent->getId(), aComponent)); + m_componentsById.insert(make_pair(component->getId(), component)); } - + /// @brief get device returns itself + /// @return shared pointer to this entity DevicePtr getDevice() const override { return getptr(); } - // Return the mapping of Device to data items + /// @brief get the data item index by id + /// @return data item index by id const auto &getDeviceDataItems() const { return m_dataItems.get(); } + /// @brief get the multi-index for data items + /// @return data item multi-index const auto &getDataItemIndex() const { return m_dataItems; } + /// @brief + /// @param[in] dataItem + /// @param[in,out] errors void addDataItem(DataItemPtr dataItem, entity::ErrorList &errors) override; - std::vector m_adapters; - + /// @brief get the version of this device + /// @return mtconnet version auto getMTConnectVersion() const { maybeGet("mtconnectVersion"); } - // Cached data items + /// @name Cached data items + ///@{ DataItemPtr getAvailability() const { return m_availability; } DataItemPtr getAssetChanged() const { return m_assetChanged; } DataItemPtr getAssetRemoved() const { return m_assetRemoved; } DataItemPtr getAssetCount() const { return m_assetCount; } + ///@} + /// @brief set the state of the preserve uuid flag + /// @param v preserve uuid state void setPreserveUuid(bool v) { m_preserveUuid = v; } + /// @brief get the preserve uuid state + /// @return `true` if uuids are preserved bool preserveUuid() const { return m_preserveUuid; } + /// @brief associate a data item with this device + /// @param di the data item void registerDataItem(DataItemPtr di); + /// @brief associate a component with the device + /// @param c the component void registerComponent(ComponentPtr c) { m_componentsById[c->getId()] = c; } + /// @brief get the topic for this device + /// @return the uuid of the device const std::string getTopicName() const override { return *m_uuid; } protected: @@ -200,6 +248,7 @@ namespace mtconnect { DataItemIndex m_dataItems; std::unordered_map> m_componentsById; + std::vector m_adapters; }; using DevicePtr = std::shared_ptr; diff --git a/src/mtconnect/device_model/reference.hpp b/src/mtconnect/device_model/reference.hpp index 5681d114..bb91d55e 100644 --- a/src/mtconnect/device_model/reference.hpp +++ b/src/mtconnect/device_model/reference.hpp @@ -1,5 +1,5 @@ // -// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) +// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,12 +36,14 @@ namespace mtconnect { class Device; using DevicePtr = std::shared_ptr; + /// @brief Reference to a component or data item class AGENT_LIB_API Reference : public entity::Entity { public: using entity::Entity::Entity; ~Reference() override = default; + /// @brief Reference relationship for this component enum RefernceType { COMPONENT, @@ -49,15 +51,25 @@ namespace mtconnect { UNKNOWN }; + /// @brief The Entity id this component is related to + /// @return the `idRef` property const entity::Value &getIdentity() const override { return getProperty("idRef"); } static entity::FactoryPtr getFactory(); static entity::FactoryPtr getRoot(); + /// @brief resolve the `idRef` using the device + /// @param device the device void resolve(DevicePtr device); + /// @brief get component for a component reference + /// @return shared pointer to the component auto &getComponent() const { return m_component; } + /// @brief get data item for a data item reference + /// @return shared pointer to the data item auto &getDataItem() const { return m_dataItem; } + /// @brief get the reference type + /// @return data item or component relationship type auto getReferenceType() const { return m_type; } protected: diff --git a/src/mtconnect/entity/data_set.cpp b/src/mtconnect/entity/data_set.cpp index 68186567..ed14d2e4 100644 --- a/src/mtconnect/entity/data_set.cpp +++ b/src/mtconnect/entity/data_set.cpp @@ -74,6 +74,7 @@ namespace std { using namespace std; namespace mtconnect::entity { + /// @brief Functions called when parsing data sets namespace DataSetParserActions { inline static void add_entry_f(DataSet &ds, const DataSetEntry &entry) { ds.emplace(entry); } inline static void make_entry_f(DataSetEntry &entry, const string &key, @@ -98,6 +99,9 @@ namespace mtconnect::entity { BOOST_PHOENIX_ADAPT_FUNCTION(void, add_entry, DataSetParserActions::add_entry_f, 2); BOOST_PHOENIX_ADAPT_FUNCTION(void, make_entry, DataSetParserActions::make_entry_f, 3); + /// @brief Parser to turn adapter text in `key=value ...` or `key={col1=value ...}` syntax into a + /// data set. + /// @tparam It the type of the iterator used by the spirit grammar template class DataSetParser : public qi::grammar { @@ -119,6 +123,8 @@ namespace mtconnect::entity { } public: + /// @brief Create a parser + /// @param[in] table `true` if we are parsing tables DataSetParser(bool table) : DataSetParser::base_type(m_start) { using namespace qi; @@ -212,8 +218,6 @@ namespace mtconnect::entity { qi::rule m_tableEntry; }; - // Split the data set entries by space delimiters and account for the - // use of single and double quotes as well as curly braces bool DataSet::parse(const std::string &text, bool table) { using boost::spirit::ascii::space; diff --git a/src/mtconnect/entity/data_set.hpp b/src/mtconnect/entity/data_set.hpp index b92bfe9e..37131b18 100644 --- a/src/mtconnect/entity/data_set.hpp +++ b/src/mtconnect/entity/data_set.hpp @@ -32,27 +32,46 @@ namespace mtconnect::entity { struct DataSetEntry; + /// @brief A set of data set entries class AGENT_LIB_API DataSet : public std::set { public: using base = std::set; using base::base; + /// @brief Get a entry for a key + /// @tparam T the entry type + /// @param key the key + /// @return the typed value of the entry template const T &get(const std::string &key) const; + + /// @brief Get a entry for a key if it exists + /// @tparam T the entry type + /// @param key the key + /// @return optional typed value of the entry template const std::optional maybeGet(const std::string &key) const; + /// @brief Split the data set entries by space delimiters and account for the + /// use of single and double quotes as well as curly braces bool parse(const std::string &s, bool table); }; + /// @brief Data set value variant using DataSetValue = std::variant; + /// @brief Equality visitor for a DataSetValue struct DataSetValueSame { DataSetValueSame(const DataSetValue &other) : m_other(other) {} bool operator()(const DataSet &v); + + /// @brief Compare the types are the same and the values are the same + /// @tparam T the data type + /// @param v the other value + /// @return `true` if they are the same template bool operator()(const T &v) { @@ -63,17 +82,33 @@ namespace mtconnect::entity { const DataSetValue &m_other; }; + /// @brief One entry in a data set. Has necessary interface to be work with maps. struct DataSetEntry { + /// @brief Create an entry with a key and value + /// @param key the key + /// @param value the value as a string + /// @param removed `true` if the key has been removed DataSetEntry(std::string key, std::string &value, bool removed = false) : m_key(std::move(key)), m_value(std::move(value)), m_removed(removed) {} + + /// @brief Create an entry for a table with a data set value + /// @param key the key + /// @param value the value as a DataSet + /// @param removed `true` if the key has been removed DataSetEntry(std::string key, DataSet &value, bool removed = false) : m_key(std::move(key)), m_value(std::move(value)), m_removed(removed) {} + /// @brief Create an entry for a data set + /// @param key the key + /// @param value the a data set variant + /// @param removed `true` if the key has been removed DataSetEntry(std::string key, DataSetValue value, bool removed = false) : m_key(std::move(key)), m_value(std::move(value)), m_removed(removed) {} + /// @brief Create a data set entry with just a key (used for search) + /// @param key DataSetEntry(std::string key) : m_key(std::move(key)), m_value(""), m_removed(false) {} DataSetEntry(const DataSetEntry &other) = default; DataSetEntry() : m_removed(false) {} @@ -92,12 +127,21 @@ namespace mtconnect::entity { } }; + /// @brief Get a typed value from a data set + /// @tparam T the data type + /// @param key the key to search for + /// @return a typed value reference + /// @throws std::bad_variant_access when type is incorrect template const T &DataSet::get(const std::string &key) const { return std::get(find(DataSetEntry(key))->m_value); } + /// @brief Get a typed value if available + /// @tparam T they type + /// @param key the key to search for + /// @return an opton typed result template const std::optional DataSet::maybeGet(const std::string &key) const { diff --git a/src/mtconnect/entity/entity.hpp b/src/mtconnect/entity/entity.hpp index 70cdfbc4..65f4590d 100644 --- a/src/mtconnect/entity/entity.hpp +++ b/src/mtconnect/entity/entity.hpp @@ -27,7 +27,11 @@ #include "requirement.hpp" namespace mtconnect { + /// @brief Entity namespace namespace entity { + /// @brief The key for a prpoerty in a property set + /// + /// A qname with a mark that allows the property to be checked for requirements struct PropertyKey : public QName { using QName::QName; @@ -36,11 +40,16 @@ namespace mtconnect { PropertyKey(const std::string &&s) : QName(s) {} PropertyKey(const char *s) : QName(s) {} + /// @brief clears marks for this property void clearMark() const { const_cast(this)->m_mark = false; } + /// @brief sets the mark for this property void setMark() const { const_cast(this)->m_mark = true; } + + /// @brief allows factory to track required properties bool m_mark {false}; }; + /// @brief properties are a map of PropertyKey to Value using Properties = std::map; using OrderList = std::list; using OrderMap = std::unordered_map; @@ -48,6 +57,11 @@ namespace mtconnect { using Property = std::pair; using AttributeSet = std::set; + /// @brief Get a property by name if it exists + /// @tparam T The property type + /// @param[in] key the key to get + /// @param[in] props the set of properties + /// @return the property value or std::nullopt template inline std::optional OptionallyGet(const std::string &key, const Properties &props) { @@ -58,28 +72,52 @@ namespace mtconnect { return std::nullopt; } + /// @brief The base entity class + /// + /// The Entity is the foundation of the all information models used by the agent. An entity can + /// be parsed or serialized as XML or JSON. The entity provides a factory and requirement + /// capability to validate the model. class AGENT_LIB_API Entity : public std::enable_shared_from_this { public: using super = std::nullptr_t; + /// @brief Create an empty entity Entity() {} + /// @brief Create an entity with a name + /// @param name entity name Entity(const std::string &name) : m_name(name) {} + /// @brief Create an entity with a name and property set + /// @param name entity name + /// @param props entity properties Entity(const std::string &name, const Properties &props) : m_name(name), m_properties(props) {} Entity(const Entity &entity) = default; virtual ~Entity() {} + /// @brief Get a shared pointer + /// @return shared pointer to the entity EntityPtr getptr() const { return const_cast(this)->shared_from_this(); } + /// @brief method to return the entities identity. defaults to `id`. + /// @return the identity virtual const entity::Value &getIdentity() const { return getProperty("id"); } + /// @brief checks if there is entity is a list + /// @return `true` if there is a LIST attribute bool hasListWithAttribute() const { return (m_properties.count("LIST") > 0 && m_properties.size() > 1); } + /// @brief get the name of the entity + /// @return name const auto &getName() const { return m_name; } + /// @brief get a const reference to the properties + /// @return properties const Properties &getProperties() const { return m_properties; } + /// @brief get a property for a ley + /// @param n the key + /// @return The property or a Value with std::monstate() if not found const Value &getProperty(const std::string &n) const { static Value noValue {std::monostate()}; @@ -89,26 +127,47 @@ namespace mtconnect { else return it->second; } + /// @brief set a property + /// @param key property key + /// @param v property value virtual void setProperty(const std::string &key, const Value &v) { m_properties.insert_or_assign(key, v); } + /// @brief set a property using a key/value pair + /// @param property the key/value pair void setProperty(const Property &property) { setProperty(property.first, property.second); } + /// @brief check if a propery exists + /// @param n the key + /// @return `true` if the property exists bool hasProperty(const std::string &n) const { return m_properties.find(n) != m_properties.end(); } + /// @brief checks if there is a `VALUE` property + /// @return `true` if there is a `VALUE` bool hasValue() const { return hasProperty("VALUE"); } + /// @brief set the name to a string + /// @param name the name void setName(const std::string &name) { m_name = name; } + /// @brief set the name as a qname + /// @param name the qname void setQName(const std::string &name) { m_name.setQName(name); } + /// @brief apply function f to the property if it exists + /// @param name the key + /// @param f the lambda to be called if the property exists void applyTo(const std::string &name, std::function f) { auto p = m_properties.find(name); if (p != m_properties.end()) f(p->second); } + /// @brief apply a function to a `VALUE` property + /// @param f the function void applyToValue(std::function f) { applyTo("VALUE", f); } + /// @brief get the `VALUE` property if it exists, Value is mutable + /// @return `VALUE` property Value &getValue() { static Value null; @@ -118,7 +177,15 @@ namespace mtconnect { else return null; } + /// @brief get a const reference to the `VALUE` property const Value &getValue() const { return getProperty("VALUE"); } + /// @brief get the entity with a list property if it exists + /// + /// This is a convience method that gets the property by name. If the property is an + /// `EntityPtr` and has a `LIST` property, then return the `EntityList` value. + /// + /// @param name the key for the list + /// @return the entity list or std::nullopt std::optional getList(const std::string &name) const { auto &v = getProperty(name); @@ -134,53 +201,107 @@ namespace mtconnect { return std::nullopt; } + /// @brief Add an entity to an entity list + /// @param name the property key + /// @param factory the factory for the entity + /// @param entity entity to add + /// @param errors errors if add fails + /// @return `true` if successful bool addToList(const std::string &name, FactoryPtr factory, EntityPtr entity, ErrorList &errors); + /// @brief Remove an entity from an entity list + /// @param name the key for the entity list + /// @param entity the entity to remove + /// @return `true` if successful bool removeFromList(const std::string &name, EntityPtr entity); + /// @brief sets the `VALUE` property + /// @param v the value void setValue(const Value &v) { setProperty("VALUE", v); } + /// @brief remove a property + /// @param name the key void erase(const std::string &name) { m_properties.erase(name); } + /// @brief get a property for a key + /// @tparam T the type of the property + /// @param name the key + /// @return the value as type T + /// @throws `std::bad_variant_access` if incorrect type template const T &get(const std::string &name) const { return std::get(getProperty(name)); } + /// @brief get the `VALUE` property + /// @tparam T the type of the value + /// @return the value as type T + /// @throws `std::bad_variant_access` if incorrect type template const T &getValue() const { return std::get(getValue()); } + /// @brief gets property if it exists + /// @tparam T the property type + /// @param name the key + /// @return the value of the property as type T or nullopt + /// @throws `std::bad_variant_access` if incorrect type template const std::optional maybeGet(const std::string &name) const { return OptionallyGet(name, m_properties); } + /// @brief gets `VALUE` property if it exists + /// @tparam T the property type + /// @return the value as type T or nullopt + /// @throws `std::bad_variant_access` if incorrect type template const std::optional maybeGetValue() const { return OptionallyGet("VALUE", m_properties); } + /// @brief Sets the entity property ordering map + /// @param[in] order the property order void setOrder(const OrderMapPtr order) { if (!m_order) m_order = order; } + /// @brief get the property order map const OrderMapPtr getOrder() const { return m_order; } + /// @brief get an iterator to the property + /// @param[in] name the key + /// @return an iterator to the property auto find(const std::string &name) { return m_properties.find(name); } + /// @brief erase a propery using an iterator + /// @param[in] it an iterator pointing to the propery + /// @return the iterator after erase auto erase(Properties::iterator &it) { return m_properties.erase(it); } + /// @brief tells the entity which properties are attributes for XML generation + /// @param[in] a the attributes void setAttributes(AttributeSet a) { m_attributes = a; } + /// @brief get the attributes for XML generation + /// @return attribute set const auto &getAttributes() const { return m_attributes; } + /// @brief compare two entities for equality + /// @param other the other entity + /// @return `true` if they have equal name and properties bool operator==(const Entity &other) const; + /// @brief compare two entities for inequality + /// @param other the other entity + /// @return `true` if they have unequal name and properties bool operator!=(const Entity &other) const { return !(*this == other); } + /// @brief update this entity to be the same as other + /// @param other the other entity + /// @param protect properties to protect from change + /// @return `true` if successful bool reviseTo(const EntityPtr other, const std::set protect = {}); - // Entity Factory protected: Value &getProperty_(const std::string &name) { @@ -199,6 +320,7 @@ namespace mtconnect { AttributeSet m_attributes; }; + /// @brief variant visitor to compare two entities for equality struct ValueEqualVisitor { ValueEqualVisitor(const Value &t) : m_this(t) {} @@ -277,6 +399,7 @@ namespace mtconnect { return true; } + /// @brief variant visitor to merge two entities struct ValueMergeVisitor { ValueMergeVisitor(Value &t, const std::set protect) diff --git a/src/mtconnect/entity/factory.cpp b/src/mtconnect/entity/factory.cpp index 9b42b28b..a03dd885 100644 --- a/src/mtconnect/entity/factory.cpp +++ b/src/mtconnect/entity/factory.cpp @@ -150,7 +150,7 @@ namespace mtconnect { p->first.setMark(); try { - if (!r.isMetBy(p->second, m_isList)) + if (!r.isMetBy(p->second)) { success = false; } diff --git a/src/mtconnect/entity/factory.hpp b/src/mtconnect/entity/factory.hpp index bff6bb13..db769f4c 100644 --- a/src/mtconnect/entity/factory.hpp +++ b/src/mtconnect/entity/factory.hpp @@ -28,6 +28,7 @@ namespace mtconnect { namespace entity { using Requirements = std::list; + /// @brief Factory for creating entities class AGENT_LIB_API Factory : public Matcher, public std::enable_shared_from_this { public: @@ -38,7 +39,10 @@ namespace mtconnect { using MatchFactory = std::list; public: - // Factory Methods + /// @brief factory method to create an `Entity` + /// @param name name of the entity + /// @param p properties for the entity + /// @return shared entity pointer static auto createEntity(const std::string &name, Properties &p) { return std::make_shared(name, p); @@ -47,19 +51,33 @@ namespace mtconnect { Factory(const Factory &other) = default; Factory() : m_function(createEntity) {} ~Factory() = default; + + /// @brief create a factory with a set of requirements + /// @param r Factory(const Requirements r) : m_requirements(r), m_function(createEntity) { registerEntityRequirements(); } + /// @brief create a factory with a set of requirements and a factory lambda + /// @param r the requirements + /// @param f the factory method Factory(const Requirements r, Function f) : m_requirements(r), m_function(f) { registerEntityRequirements(); } + /// @brief copy the factory and all the related factories + /// @return a new factory FactoryPtr deepCopy() const; + /// @brief get a shared pointer to this factory + /// @return shared factory pointer FactoryPtr getptr() { return shared_from_this(); } + /// @brief set the factory function + /// @param f the factory function void setFunction(Function f) { m_function = f; } + /// @brief set the entity parameter order + /// @param list the ordered list of parameters void setOrder(OrderList list) { m_order = std::make_shared(); @@ -67,28 +85,62 @@ namespace mtconnect { for (auto &e : list) m_order->emplace(e, i++); } + /// @brief set the order from a order map + /// @param list the order map void setOrder(OrderMapPtr &list) { m_order = list; } + /// @brief get the order list + /// @return pointer to the order list const OrderMapPtr &getOrder() const { return m_order; } + /// @brief set if this is a list factory + /// @param list `true` if this is a list void setList(bool list) { m_isList = list; } + /// @brief gets the list state of this factory + /// @return `true` if this is a list factory bool isList() const { return m_isList; } + /// @brief sets the raw state of this factory + /// + /// Raw does not interpret the data, but turns it into a set of entities + /// + /// @param raw `true` if it is raw void setHasRaw(bool raw) { m_hasRaw = raw; } + /// @brief gets raw state + /// @return `true` if the factory creates a raw entity bool hasRaw() const { return m_hasRaw; } + /// @brief indicator if this factory creates extended entities + /// @return `true` if this factory supports any types bool isAny() const { return m_any; } + /// @brief sets the any state + /// @param any `true` if supports any void setAny(bool any) { m_any = any; } + /// @brief sets the minimum size of the entity list + /// @param size minimum size of an entity list void setMinListSize(size_t size) { m_minListSize = size; m_isList = true; } + /// @brief does this factory have a requirement for the property + /// @param name the property key + /// @return `true` if there is a requirement bool isProperty(const std::string &name) const { return m_properties.count(name) > 0; } + /// @brief checks if this is a property set + /// @param name the property key + /// @return `true` if this is a property with ENTITY or ENTITY_SET with multiplicity more than + /// 0 bool isPropertySet(const std::string &name) const { return m_propertySets.count(name) > 0; } + /// @brief is there a requirement with a simple value + /// @param name the property key + /// @return `true` if this is a value propery bool isSimpleProperty(const std::string &name) const { return m_simpleProperties.count(name) > 0; } + /// @brief get the requirement pointer for a key + /// @param name the property key + /// @return requirement pointer Requirement *getRequirement(const std::string &name) { for (auto &r : m_requirements) @@ -99,6 +151,8 @@ namespace mtconnect { return nullptr; } + /// @brief add requirements to the factory + /// @param reqs the set of requirements void addRequirements(const Requirements &reqs) { for (const auto &r : reqs) @@ -116,9 +170,24 @@ namespace mtconnect { } registerEntityRequirements(); } + /// @brief convert properties to the requirements + /// @param[in,out] p the properties to convert + /// @param[in,out] errors errors related to conversions void performConversions(Properties &p, ErrorList &errors) const; + /// @brief check if the properties are sufficient for the factory + /// @param[in,out] properties the properties for the entity + /// @param[in,out] errors errors related to verification + /// @return `true` if the properties are sufficient virtual bool isSufficient(Properties &properties, ErrorList &errors) const; + /// @name Entity factory + ///@{ + + /// @brief make an entity given an entity name and properties + /// @param[in] name the name of the entity + /// @param[in,out] p properties for the entity + /// @param[in,out] errors errors when creating the entity + /// @return shared entity pointer if successful EntityPtr make(const std::string &name, Properties &p, ErrorList &errors) const { try @@ -149,26 +218,46 @@ namespace mtconnect { return nullptr; } - // Factory + /// @brief alias for `make()` + EntityPtr operator()(const std::string &name, Properties &p, ErrorList &errors) const + { + return make(name, p, errors); + } + + ///@} + + /// @brief add a factory for entities with a string matcher + /// @param name the name to match against + /// @param factory the factory to create entities + /// @return `true` if successful bool registerFactory(const std::string &name, FactoryPtr factory) { m_stringFactory.emplace(make_pair(name, factory)); return true; } - + /// @brief add a factory for entities with a regular expression matcher + /// @param exp expression to match against + /// @param factory the factory to create entities + /// @return `true` if successful bool registerFactory(const std::regex &exp, FactoryPtr factory) { auto matcher = [exp](const std::string &name) { return std::regex_match(name, exp); }; m_matchFactory.emplace_back(make_pair(matcher, factory)); return true; } - + /// @brief add a factory for entities with custom matcher + /// @param matcher matcher to use to match + /// @param factory the factory to create entities + /// @return `true` if successful bool registerFactory(const Matcher &matcher, FactoryPtr factory) { m_matchFactory.emplace_back(make_pair(matcher, factory)); return true; } + /// @brief find a factory for a name + /// @param name the name to match + /// @return the factory FactoryPtr factoryFor(const std::string &name) const { const auto it = m_stringFactory.find(name); @@ -186,18 +275,27 @@ namespace mtconnect { return nullptr; } + /// @brief check if a factory exists + /// @param s name to match against + /// @return `true` if there is a factory bool matches(const std::string &s) const override { auto f = factoryFor(s); return (bool)f; } - EntityPtr operator()(const std::string &name, Properties &p, ErrorList &errors) const - { - return make(name, p, errors); - } - - std::shared_ptr create(const std::string &name, EntityList &a, ErrorList &errors) + /// @name Methods to create entities + ///@{ + + /// @brief create an entity with an entity list + /// + /// Looks for a factory for name + /// + /// @param name the name of the entity + /// @param a list of entities + /// @param errors errors when creating entity + /// @return entity if successful + EntityPtr create(const std::string &name, EntityList &a, ErrorList &errors) { auto factory = factoryFor(name); if (factory) @@ -209,7 +307,15 @@ namespace mtconnect { return nullptr; } - std::shared_ptr create(const std::string &name, Properties &a, ErrorList &errors) + /// @brief create an entity with properties + /// + /// Looks for a factory for name + /// + /// @param[in] name the entity name + /// @param[in,out] a the properties + /// @param[in,out] errors errors when creating entity + /// @return entity if successful + EntityPtr create(const std::string &name, Properties &a, ErrorList &errors) { auto factory = factoryFor(name); if (factory) @@ -217,7 +323,16 @@ namespace mtconnect { else return nullptr; } - std::shared_ptr create(const std::string &name, Properties &&a, ErrorList &errors) + + /// @brief create an entity with properties as rvalue + /// + /// Looks for a factory for name + /// + /// @param name the entity name + /// @param[in,out] a the properties as an rvalue + /// @param[in,out] errors errors when creating entity + /// @return entity if successful + EntityPtr create(const std::string &name, Properties &&a, ErrorList &errors) { auto factory = factoryFor(name); if (factory) @@ -226,12 +341,22 @@ namespace mtconnect { return nullptr; } - std::shared_ptr create(const std::string &name, Properties &a) + /// @brief create an entity with properties as rvalue + /// + /// Suppresses errors. + /// + /// @param name the entity name + /// @param[in,out] a the properties as an rvalue + /// @return entity if successful + EntityPtr create(const std::string &name, Properties &a) { ErrorList list; return create(name, a, list); } + ///@} + + /// @brief scan requirements and register factories refrenced in the requirements void registerEntityRequirements() { for (auto &r : m_requirements) @@ -255,6 +380,7 @@ namespace mtconnect { } } + /// @brief register matchers for requirements to this factory if not given void registerMatchers() { auto m = getptr(); @@ -267,12 +393,16 @@ namespace mtconnect { } } - // For testing + /// @name Testing + ///@{ + + /// @brief method used only in testing void clear() { m_stringFactory.clear(); m_matchFactory.clear(); } + ///@} protected: using FactoryMap = std::map; diff --git a/src/mtconnect/entity/json_parser.hpp b/src/mtconnect/entity/json_parser.hpp index 9e4e1652..90f2bb01 100644 --- a/src/mtconnect/entity/json_parser.hpp +++ b/src/mtconnect/entity/json_parser.hpp @@ -1,5 +1,5 @@ // -// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) +// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,12 +29,23 @@ namespace mtconnect { namespace entity { + /// @brief Parser to convert a JSON document to an entity class AGENT_LIB_API JsonParser { public: + /// @brief Create a json parser + /// @param version the supported MTConnect serialization version + /// - Version 1 has a repreated objects in arrays for collections of objects + /// - Version 2 combines arrays of objects by type JsonParser(uint32_t version = 1) : m_version(version) {} ~JsonParser() = default; + /// @brief Parse a JSON document to an entity + /// @param factory The top level factory for parsing + /// @param document The json document + /// @param version the version to parse + /// @param errors Errors that occurred creating the entities + /// @return an entity shared pointer if successful EntityPtr parse(FactoryPtr factory, const std::string &document, const std::string &version, ErrorList &errors); diff --git a/src/mtconnect/entity/json_printer.hpp b/src/mtconnect/entity/json_printer.hpp index 5c84904a..d34c2694 100644 --- a/src/mtconnect/entity/json_printer.hpp +++ b/src/mtconnect/entity/json_printer.hpp @@ -26,15 +26,29 @@ using json = nlohmann::json; namespace mtconnect { namespace entity { + /// @brief Serializes entities as JSON text class AGENT_LIB_API JsonPrinter { public: + /// @brief Create a json printer + /// @param version the supported MTConnect serialization version + /// - Version 1 has a repreated objects in arrays for collections of objects + /// - Version 2 combines arrays of objects by type JsonPrinter(uint32_t version) : m_version(version) {}; + /// @brief create a json object from an entity + /// + /// Cover method for `printEntity()` + /// + /// @param entity the entity + /// @return json object json print(const EntityPtr entity) const { return json::object({{entity->getName(), printEntity(entity)}}); } + /// @brief Convert properties of an entity into a json object + /// @param entity the entity + /// @return json object json printEntity(const EntityPtr entity) const; protected: diff --git a/src/mtconnect/entity/qname.hpp b/src/mtconnect/entity/qname.hpp index 30194e13..44970840 100644 --- a/src/mtconnect/entity/qname.hpp +++ b/src/mtconnect/entity/qname.hpp @@ -25,18 +25,31 @@ namespace mtconnect { namespace entity { + /// @brief Qualified name + /// + /// The qname uses the underlying string for storage and keeps a position + /// to the namespace position class AGENT_LIB_API QName : public std::string { public: QName() = default; + /// @brief Create a qualified name from name and ns + /// @param name the name + /// @param ns the namespace prefix QName(const std::string &name, const std::string &ns) { assign(ns + ":" + name); m_nsLen = ns.length(); } + /// @brief Create a qualified name from a string + /// @param qname the name QName(const std::string &qname) { setQName(qname); } + /// @brief Set the qualified name. Parses the qname and looks for a colon and splits the + /// name into the namespace prefix and the name + /// @param qname + /// @param ns void setQName(const std::string &qname, const std::optional &ns = std::nullopt) { if (ns) @@ -58,15 +71,22 @@ namespace mtconnect { } } + /// @brief copy constructor + /// @param other the source QName(const QName &other) = default; ~QName() = default; + /// @brief operator = + /// @param name the source + /// @return this qname QName &operator=(const std::string &name) { setQName(name); return *this; } + /// @brief set the name portion + /// @param name void setName(const std::string &name) { if (m_nsLen == 0) @@ -80,8 +100,12 @@ namespace mtconnect { } } + /// @brief is there a namespace + /// @return `true` if there is a namespace bool hasNs() const { return m_nsLen > 0; } + /// @brief set the namespace portion + /// @param ns the namespace void setNs(const std::string &ns) { std::string name(getName()); @@ -96,13 +120,19 @@ namespace mtconnect { } } + /// @brief clear the string and the namespace void clear() { std::string::clear(); m_nsLen = 0; } + /// @brief get this qname + /// @return this const auto &getQName() const { return *this; } + + /// @brief get a string view to the name portion of the qname + /// @return string view of the name const std::string_view getName() const { if (m_nsLen == 0) @@ -110,6 +140,8 @@ namespace mtconnect { else return std::string_view(c_str() + m_nsLen + 1); } + /// @brief get a stringview to the namespace portion + /// @return string view of the namespace or an empty string view const std::string_view getNs() const { if (m_nsLen == 0) @@ -117,6 +149,8 @@ namespace mtconnect { else return std::string_view(c_str(), m_nsLen); } + /// @brief get a pair of strings with the namespace and the name + /// @return namespace and the name const std::pair getPair() const { if (m_nsLen > 0) @@ -129,7 +163,11 @@ namespace mtconnect { } } + /// @brief get the qname as a string + /// @return this std::string &str() { return *this; } + /// @brief const get this as a string + /// @return this const std::string &str() const { return *this; } protected: diff --git a/src/mtconnect/entity/requirement.cpp b/src/mtconnect/entity/requirement.cpp index 5ac1a497..7ce5307e 100644 --- a/src/mtconnect/entity/requirement.cpp +++ b/src/mtconnect/entity/requirement.cpp @@ -57,7 +57,7 @@ namespace mtconnect { m_factory = f; } - bool Requirement::isMetBy(const Value &value, bool isList) const + bool Requirement::isMetBy(const Value &value) const { // Is this a multiple entry if ((m_type == ENTITY || m_type == ENTITY_LIST)) @@ -155,6 +155,8 @@ namespace mtconnect { return true; } + /// @brief Internal visitor to convert a value from one type to another. + /// @throws PropertyError if the value cannot be converted struct ValueConverter { ValueConverter(ValueType type, bool table) : m_type(type), m_table(table) {} diff --git a/src/mtconnect/entity/requirement.hpp b/src/mtconnect/entity/requirement.hpp index 77336aed..b68c305f 100644 --- a/src/mtconnect/entity/requirement.hpp +++ b/src/mtconnect/entity/requirement.hpp @@ -17,6 +17,9 @@ #pragma once +/// @file requirement.hpp +/// @brief Entity Requirement types and classes + #include #include #include @@ -40,240 +43,331 @@ #include "mtconnect/config.hpp" #include "mtconnect/utilities.hpp" -namespace mtconnect { - namespace entity { - class Entity; - using EntityPtr = std::shared_ptr; - using EntityList = std::list>; - using Vector = std::vector; +namespace mtconnect::entity { + class Entity; + using EntityPtr = std::shared_ptr; + /// @brief List of shared entities + using EntityList = std::list>; + /// @brief Vector of doubles + using Vector = std::vector; - using Value = std::variant; + /// @brief Entity Value variant + using Value = std::variant; - enum ValueType : std::uint16_t - { - EMPTY = 0x0, - ENTITY = 0x1, - ENTITY_LIST = 0x2, - STRING = 0x3, - INTEGER = 0x4, - DOUBLE = 0x5, - BOOL = 0x6, - VECTOR = 0x7, - DATA_SET = 0x8, - TIMESTAMP = 0x9, - NULL_VALUE = 0xA, - USTRING = 0x10 | STRING, - QSTRING = 0x20 | STRING, - TABLE = 0x10 | DATA_SET - }; + /// @brief Value type enumeration + enum ValueType : std::uint16_t + { + EMPTY = 0x0, ///< monostate for no value + ENTITY = 0x1, ///< shared entity pointer + ENTITY_LIST = 0x2, ///< list of entities + STRING = 0x3, ///< string value + INTEGER = 0x4, ///< int64 value + DOUBLE = 0x5, ///< double value + BOOL = 0x6, ///< bool value + VECTOR = 0x7, ///< Vector of doubles + DATA_SET = 0x8, ///< DataSet of key value pairs + TIMESTAMP = 0x9, ///< Timestamp in microseconds + NULL_VALUE = 0xA, ///< null pointer + USTRING = 0x10 | STRING, ///< Upper case string (represented as string) + QSTRING = 0x20 | STRING, ///< Qualified Name String (represented as string) + TABLE = 0x10 | DATA_SET ///< Table (represented as data set) + }; - const int16_t VALUE_TYPE_BASE = 0x0F; + /// @brief Mask for value types + const int16_t VALUE_TYPE_BASE = 0x0F; - class Factory; - using FactoryPtr = std::shared_ptr; - using ControlledVocab = std::list; - using Pattern = std::optional; - using VocabSet = std::optional>; + class Factory; + using FactoryPtr = std::shared_ptr; + using ControlledVocab = std::list; + using Pattern = std::optional; + using VocabSet = std::optional>; - bool AGENT_LIB_API ConvertValueToType(Value &value, ValueType type, bool table = false); + /// @brief Convert a `Value` to a given type + /// @param value The value to convert + /// @param type the target type + /// @param table special treatment if a table (data sets of data set) + /// @return `true` if conversion was successful + bool AGENT_LIB_API ConvertValueToType(Value &value, ValueType type, bool table = false); - class AGENT_LIB_API EntityError : public std::logic_error - { - public: - explicit EntityError(const std::string &s, const std::string &e = "") - : std::logic_error(s), m_entity(e) - {} + /// @brief Error class when an error occurred + class AGENT_LIB_API EntityError : public std::logic_error + { + public: + explicit EntityError(const std::string &s, const std::string &e = "") + : std::logic_error(s), m_entity(e) + {} - explicit EntityError(const char *s, const std::string &e = "") - : std::logic_error(s), m_entity(e) - {} + explicit EntityError(const char *s, const std::string &e = "") + : std::logic_error(s), m_entity(e) + {} - EntityError(const EntityError &o) noexcept : std::logic_error(o), m_entity(o.m_entity) {} - ~EntityError() override = default; + EntityError(const EntityError &o) noexcept : std::logic_error(o), m_entity(o.m_entity) {} + ~EntityError() override = default; - virtual const char *what() const noexcept override - { - if (m_text.empty()) - { - auto *t = const_cast(this); - t->m_text = m_entity + ": " + std::logic_error::what(); - } - return m_text.c_str(); - } - void setEntity(const std::string &s) + /// @brief an error related to an entity + /// @return the error text + virtual const char *what() const noexcept override + { + if (m_text.empty()) { - m_text.clear(); - m_entity = s; + auto *t = const_cast(this); + t->m_text = m_entity + ": " + std::logic_error::what(); } - virtual EntityError *dup() const noexcept { return new EntityError(*this); } - const std::string &getEntity() const { return m_entity; } + return m_text.c_str(); + } + /// @brief set the entity text + /// @param[in] s the entity text + void setEntity(const std::string &s) + { + m_text.clear(); + m_entity = s; + } + virtual EntityError *dup() const noexcept { return new EntityError(*this); } + const std::string &getEntity() const { return m_entity; } - protected: - std::string m_text; - std::string m_entity; - }; + protected: + std::string m_text; + std::string m_entity; + }; - class AGENT_LIB_API PropertyError : public EntityError - { - public: - explicit PropertyError(const std::string &s, const std::string &p = "", - const std::string &e = "") - : EntityError(s, e), m_property(p) - {} + /// @brief an error related to an entity property + class AGENT_LIB_API PropertyError : public EntityError + { + public: + explicit PropertyError(const std::string &s, const std::string &p = "", + const std::string &e = "") + : EntityError(s, e), m_property(p) + {} - explicit PropertyError(const char *s, const std::string &p = "", const std::string &e = "") - : EntityError(s, e), m_property(p) - {} + explicit PropertyError(const char *s, const std::string &p = "", const std::string &e = "") + : EntityError(s, e), m_property(p) + {} - PropertyError(const PropertyError &o) noexcept : EntityError(o), m_property(o.m_property) {} - ~PropertyError() override = default; + PropertyError(const PropertyError &o) noexcept : EntityError(o), m_property(o.m_property) {} + ~PropertyError() override = default; - virtual const char *what() const noexcept override - { - if (m_text.empty()) - { - auto *t = const_cast(this); - t->m_text = m_entity + "(" + m_property + "): " + std::logic_error::what(); - } - return m_text.c_str(); - } - void setProperty(const std::string &p) + virtual const char *what() const noexcept override + { + if (m_text.empty()) { - m_text.clear(); - m_property = p; + auto *t = const_cast(this); + t->m_text = m_entity + "(" + m_property + "): " + std::logic_error::what(); } - EntityError *dup() const noexcept override { return new PropertyError(*this); } - const std::string &getProperty() const { return m_property; } + return m_text.c_str(); + } + void setProperty(const std::string &p) + { + m_text.clear(); + m_property = p; + } + EntityError *dup() const noexcept override { return new PropertyError(*this); } + const std::string &getProperty() const { return m_property; } - protected: - std::string m_property; - }; + protected: + std::string m_property; + }; - using ErrorList = std::list>; + using ErrorList = std::list>; - struct Matcher - { - virtual ~Matcher() = default; - virtual bool matches(const std::string &s) const = 0; - }; + /// @brief A simple class that can be subclassed to customize matching + struct Matcher + { + virtual ~Matcher() = default; + virtual bool matches(const std::string &s) const = 0; + }; + + using MatcherPtr = std::weak_ptr; - using MatcherPtr = std::weak_ptr; + /// @brief A requirement for a an entity property + class AGENT_LIB_API Requirement + { + public: + /// @brief tag to use for limitless occurrences + const static auto Infinite {std::numeric_limits::max()}; - class AGENT_LIB_API Requirement + public: + /// @brief property requirement with a type that can be optional + /// @param name the property key + /// @param type the data type + /// @param required `true` if the property is required + Requirement(const std::string &name, ValueType type, bool required = true) + : m_name(name), m_upperMultiplicity(1), m_lowerMultiplicity(required ? 1 : 0), m_type(type) + {} + /// @brief property requirement with a type that can be optional + /// @param name the property key + /// @param required `true` if the property is required + /// @param type the data type defaulted to `STRING` + Requirement(const std::string &name, bool required, ValueType type = STRING) + : m_name(name), m_upperMultiplicity(1), m_lowerMultiplicity(required ? 1 : 0), m_type(type) + {} + /// @brief property that can occur mode than once + /// @param name the property key + /// @param type they data type + /// @param lower a lower bound occurrence + /// @param upper an upper bound occurrence + Requirement(const std::string &name, ValueType type, int lower, int upper) + : m_name(name), m_upperMultiplicity(upper), m_lowerMultiplicity(lower), m_type(type) + {} + /// @brief property with required vector size + /// @param name the property key + /// @param type the data type + /// @param size the size of the value + /// @param required `true` if the property is required + Requirement(const std::string &name, ValueType type, int size, bool required = true) + : m_name(name), + m_upperMultiplicity(1), + m_lowerMultiplicity(required ? 1 : 0), + m_size(size), + m_type(type) + {} + /// @brief property requirement for an entity or entity list with a factory + /// @param name the property key + /// @param type the data type. `ENTITY` or `ENTITY_LIST` + /// @param o the entity factory + /// @param required `true` if the property is required + Requirement(const std::string &name, ValueType type, FactoryPtr o, bool required = true); + /// @brief property requirement for an entity or entity list + /// @param name the property key + /// @param type the data type. `ENTITY` or `ENTITY_LIST` + /// @param o the entity factory + /// @param lower lower bound for multiplicity + /// @param upper upper bound for multiplicity + Requirement(const std::string &name, ValueType type, FactoryPtr o, int lower, int upper); + /// @brief property requirement for a string value that must match a controlled vocabulary + /// @param name the property key + /// @param vocab the set of possible values + /// @param required `true` if the property is required + Requirement(const std::string &name, const ControlledVocab &vocab, bool required = true) + : m_name(name), m_upperMultiplicity(1), m_lowerMultiplicity(required ? 1 : 0), m_type(STRING) { - public: - const static auto Infinite {std::numeric_limits::max()}; + m_vocabulary.emplace(); + for (auto &s : vocab) + m_vocabulary->emplace(s); + } + /// @brief propery requirement where the text must match a regex pattern + /// @param name the property key + /// @param pattern the regex + /// @param required `true` if the property is required + Requirement(const std::string &name, const std::regex &pattern, bool required = true) + : m_name(name), + m_upperMultiplicity(1), + m_lowerMultiplicity(required ? 1 : 0), + m_type(STRING), + m_pattern(pattern) + {} - public: - Requirement(const std::string &name, ValueType type, bool required = true) - : m_name(name), m_upperMultiplicity(1), m_lowerMultiplicity(required ? 1 : 0), m_type(type) - {} - Requirement(const std::string &name, bool required, ValueType type = STRING) - : m_name(name), m_upperMultiplicity(1), m_lowerMultiplicity(required ? 1 : 0), m_type(type) - {} - Requirement(const std::string &name, ValueType type, int lower, int upper) - : m_name(name), m_upperMultiplicity(upper), m_lowerMultiplicity(lower), m_type(type) - {} - Requirement(const std::string &name, ValueType type, int size, bool required = true) - : m_name(name), - m_upperMultiplicity(1), - m_lowerMultiplicity(required ? 1 : 0), - m_size(size), - m_type(type) - {} - Requirement(const std::string &name, ValueType type, FactoryPtr o, bool required = true); - Requirement(const std::string &name, ValueType type, FactoryPtr o, int lower, int upper); - Requirement(const std::string &name, const ControlledVocab &vocab, bool required = true) - : m_name(name), - m_upperMultiplicity(1), - m_lowerMultiplicity(required ? 1 : 0), - m_type(STRING) - { - m_vocabulary.emplace(); - for (auto &s : vocab) - m_vocabulary->emplace(s); - } - Requirement(const std::string &name, const std::regex &pattern, bool required = true) - : m_name(name), - m_upperMultiplicity(1), - m_lowerMultiplicity(required ? 1 : 0), - m_type(STRING), - m_pattern(pattern) - {} + Requirement() = default; + Requirement(const Requirement &o) = default; + ~Requirement() = default; - Requirement() = default; - Requirement(const Requirement &o) = default; - ~Requirement() = default; + /// @brief Check if two requires are the same + /// @param o another requirement + /// @return `true` if they are equal + Requirement &operator=(const Requirement &o) + { + m_type = o.m_type; + m_lowerMultiplicity = o.m_lowerMultiplicity; + m_upperMultiplicity = o.m_upperMultiplicity; + m_factory = o.m_factory; + m_matcher = o.m_matcher; + m_size = o.m_size; + return *this; + } - Requirement &operator=(const Requirement &o) + /// @brief gets required state + /// @return `true` if property is required + bool isRequired() const { return m_lowerMultiplicity > 0; } + /// @brief get optional state + /// @return `true` if property is options + bool isOptional() const { return !isRequired(); } + /// @brief gets the upper multiplicity + /// @return upper multiplicity + int getUpperMultiplicity() const { return m_upperMultiplicity; } + /// @brief gets the lower multiplicity + /// @return lower multiplicity + int getLowerMultiplicity() const { return m_lowerMultiplicity; } + /// @brief gets the size if set + /// @return the size + std::optional getSize() const { return m_size; } + /// @brief gets the matcher + /// @return the matcher + const auto &getMatcher() const { return m_matcher; } + /// @brief sets the matcher for this requirement + /// @param[in] m a shared pointer to the matcher + void setMatcher(MatcherPtr m) { m_matcher = m; } + /// @brief gets the name of the requirement + /// @return the name for the property key + const std::string &getName() const { return m_name; } + /// @brief gets the value type for the requirement + /// @return the value type + ValueType getType() const { return m_type; } + /// @brief gets the factory for elements and element lists + /// @return the factory + auto &getFactory() const { return m_factory; } + /// @brief sets the factory for an entity and entity list + /// @param f the factory + void setFactory(FactoryPtr &f) { m_factory = f; } + /// @brief set the multiplicity + /// @param lower the upper multiplicity + /// @param upper the lower multiplicity + void setMultiplicity(int lower, int upper) + { + m_upperMultiplicity = upper; + m_lowerMultiplicity = lower; + } + /// @brief makes this requirement required + void makeRequired() { m_lowerMultiplicity = 1; } + + /// @brief convert a given value to the requirement type + /// @param v the value + /// @param table if this is a table conversion + /// @return `true` if it is successful + bool convertType(Value &v, bool table = false) const + { + try { - m_type = o.m_type; - m_lowerMultiplicity = o.m_lowerMultiplicity; - m_upperMultiplicity = o.m_upperMultiplicity; - m_factory = o.m_factory; - m_matcher = o.m_matcher; - m_size = o.m_size; - return *this; + return ConvertValueToType(v, m_type, table); } - - bool isRequired() const { return m_lowerMultiplicity > 0; } - bool isOptional() const { return !isRequired(); } - int getUpperMultiplicity() const { return m_upperMultiplicity; } - int getLowerMultiplicity() const { return m_lowerMultiplicity; } - std::optional getSize() const { return m_size; } - const auto &getMatcher() const { return m_matcher; } - void setMatcher(MatcherPtr m) { m_matcher = m; } - const std::string &getName() const { return m_name; } - ValueType getType() const { return m_type; } - auto &getFactory() const { return m_factory; } - void setFactory(FactoryPtr &f) { m_factory = f; } - void setMultiplicity(int lower, int upper) + catch (PropertyError &e) { - m_upperMultiplicity = upper; - m_lowerMultiplicity = lower; + e.setProperty(m_name); + throw e; } - void makeRequired() { m_lowerMultiplicity = 1; } - - bool convertType(Value &v, bool table = false) const + return false; + } + /// @brief does this have a given matcher + /// @return `true` if it has a matcher + bool hasMatcher() const { return m_matcher.use_count() > 0; } + /// @brief does a value meet the requirement + /// @param value the value + /// @return `true` if the requirement is met + bool isMetBy(const Value &value) const; + /// @brief checks if a string matches the requirement + /// @param s the string to check + /// @return `true` if it matches + bool matches(const std::string &s) const + { + if (auto m = m_matcher.lock()) { - try - { - return ConvertValueToType(v, m_type, table); - } - catch (PropertyError &e) - { - e.setProperty(m_name); - throw e; - } - return false; + return m->matches(s); } - bool hasMatcher() const { return m_matcher.use_count() > 0; } - bool isMetBy(const Value &value, bool isList) const; - bool matches(const std::string &s) const + else { - if (auto m = m_matcher.lock()) - { - return m->matches(s); - } - else - { - return m_name == s; - } + return m_name == s; } + } - protected: - std::string m_name; - int m_upperMultiplicity; - int m_lowerMultiplicity; - std::optional m_size; - ValueType m_type; - MatcherPtr m_matcher; - FactoryPtr m_factory; - Pattern m_pattern; - VocabSet m_vocabulary; - }; + protected: + std::string m_name; + int m_upperMultiplicity; + int m_lowerMultiplicity; + std::optional m_size; + ValueType m_type; + MatcherPtr m_matcher; + FactoryPtr m_factory; + Pattern m_pattern; + VocabSet m_vocabulary; + }; - // Inlines - } // namespace entity -} // namespace mtconnect +} // namespace mtconnect::entity diff --git a/src/mtconnect/entity/xml_parser.hpp b/src/mtconnect/entity/xml_parser.hpp index 3e8cca74..5c8c346f 100644 --- a/src/mtconnect/entity/xml_parser.hpp +++ b/src/mtconnect/entity/xml_parser.hpp @@ -30,6 +30,7 @@ struct _xmlNode; namespace mtconnect { namespace entity { + /// @brief Parse an XML document to an entity class AGENT_LIB_API XmlParser { public: @@ -37,8 +38,21 @@ namespace mtconnect { ~XmlParser() = default; using xmlNodePtr = _xmlNode *; + /// @brief Parse an xmlNodePointer (libxml2) to an entity + /// @param factory The factory to use to create the top level entity + /// @param node an libxml2 node + /// @param errors errors that occurred during the parsing + /// @param parseNamespaces `true` if namespaces should be parsed + /// @return a shared pointer to an entity if successful static EntityPtr parseXmlNode(FactoryPtr factory, xmlNodePtr node, ErrorList &errors, bool parseNamespaces = true); + /// @brief Parse a string document to an entity + /// @param factory The factory to use to create the top level entity + /// @param document the document as a string + /// @param version version to parse + /// @param errors errors that occurred during the parsing + /// @param parseNamespaces `true` if namespaces should be parsed + /// @return a shared pointer to an entity if successful static EntityPtr parse(FactoryPtr factory, const std::string &document, const std::string &version, ErrorList &errors, bool parseNamespaces = true); diff --git a/src/mtconnect/entity/xml_printer.hpp b/src/mtconnect/entity/xml_printer.hpp index 1d92435a..959fc8ec 100644 --- a/src/mtconnect/entity/xml_printer.hpp +++ b/src/mtconnect/entity/xml_printer.hpp @@ -30,11 +30,16 @@ extern "C" namespace mtconnect { namespace entity { + /// @brief Convert an entity to an XML document class AGENT_LIB_API XmlPrinter { public: XmlPrinter() = default; + /// @brief convert an entity to a XML document using `libxml2` + /// @param writer libxml2 `xmlTextWriterPtr` + /// @param entity the entity + /// @param namespaces a set of namespaces to use in the document void print(xmlTextWriterPtr writer, const EntityPtr entity, const std::unordered_set &namespaces); }; diff --git a/src/mtconnect/logging.hpp b/src/mtconnect/logging.hpp index b420259f..7240853b 100644 --- a/src/mtconnect/logging.hpp +++ b/src/mtconnect/logging.hpp @@ -15,6 +15,9 @@ // limitations under the License. // +/// @file logging.hpp +/// @brief common logging macros based on boost trivial log + #pragma once #include @@ -22,7 +25,10 @@ #include "mtconnect/config.hpp" +/// @brief synonym for `BOOST_LOG_TRIVIAL` #define LOG BOOST_LOG_TRIVIAL + +/// @brief synonym for `BOOST_LOG_NAMED_SCOPE` #define NAMED_SCOPE BOOST_LOG_NAMED_SCOPE // Must be initialized in the plugin before callign log as follows: @@ -35,6 +41,7 @@ namespace mtconnect { } // namespace configuration } // namespace mtconnect +/// @brief Used when static using static agent_lib in a plugin shared object #define PLUGIN_LOG(lvl) \ BOOST_LOG_STREAM_WITH_PARAMS(*mtconnect::configuration::gAgentLogger, \ (::boost::log::keywords::severity = ::boost::log::trivial::lvl)) diff --git a/src/mtconnect/mqtt/mqtt_server.hpp b/src/mtconnect/mqtt/mqtt_server.hpp index 95231374..13f6cc03 100644 --- a/src/mtconnect/mqtt/mqtt_server.hpp +++ b/src/mtconnect/mqtt/mqtt_server.hpp @@ -18,10 +18,14 @@ #include "mtconnect/source/adapter/adapter_pipeline.hpp" namespace mtconnect { + /// @brief MQTT Server namespace namespace mqtt_server { + /// @brief class MqttServer : public std::enable_shared_from_this { public: + /// @brief + /// @param ioc MqttServer(boost::asio::io_context &ioc) : m_ioContext(ioc), m_port(1883) {} virtual ~MqttServer() = default; const auto &getUrl() const { return m_url; } diff --git a/src/mtconnect/observation/change_observer.hpp b/src/mtconnect/observation/change_observer.hpp index fbb4c383..e64754d2 100644 --- a/src/mtconnect/observation/change_observer.hpp +++ b/src/mtconnect/observation/change_observer.hpp @@ -27,85 +27,107 @@ #include "mtconnect/config.hpp" #include "mtconnect/utilities.hpp" -namespace mtconnect { - namespace observation { - class ChangeSignaler; - class AGENT_LIB_API ChangeObserver +namespace mtconnect::observation { + class ChangeSignaler; + + /// @brief A class to observe a data item and singal when data changes + class AGENT_LIB_API ChangeObserver + { + public: + /// @brief Create a change observer that runs in a strand + /// @param[in] strand the strand + ChangeObserver(boost::asio::io_context::strand &strand) + : m_strand(strand), m_timer(strand.context()) + {} + + virtual ~ChangeObserver(); + + /// @brief wait for a change to occur asynchronously + /// @param duration the duration to wait + /// @param handler the handler to call back + /// @return `true` if successful + bool wait(std::chrono::milliseconds duration, + std::function handler) { - public: - ChangeObserver(boost::asio::io_context::strand &strand) - : m_strand(strand), m_timer(strand.context()) - {} + std::unique_lock lock(m_mutex); - virtual ~ChangeObserver(); - - bool wait(std::chrono::milliseconds duration, - std::function handler) + if (m_sequence != UINT64_MAX) { - std::unique_lock lock(m_mutex); - - if (m_sequence != UINT64_MAX) - { - boost::asio::post(boost::asio::bind_executor( - m_strand, boost::bind(handler, boost::system::error_code {}))); - } - else - { - m_timer.expires_from_now(duration); - m_timer.async_wait(handler); - } - return true; + boost::asio::post(boost::asio::bind_executor( + m_strand, boost::bind(handler, boost::system::error_code {}))); } - - void signal(uint64_t sequence) - { - std::lock_guard scopedLock(m_mutex); - - if (m_sequence > sequence && sequence) - m_sequence = sequence; - - m_timer.cancel(); - } - - uint64_t getSequence() const { return m_sequence; } - - bool wasSignaled() const { return m_sequence != UINT64_MAX; } - - void reset() + else { - std::lock_guard scopedLock(m_mutex); - m_sequence = UINT64_MAX; + m_timer.expires_from_now(duration); + m_timer.async_wait(handler); } - - private: - boost::asio::io_context::strand &m_strand; - mutable std::recursive_mutex m_mutex; - boost::asio::steady_timer m_timer; - - std::list m_signalers; - volatile uint64_t m_sequence = UINT64_MAX; - - protected: - friend class ChangeSignaler; - void addSignaler(ChangeSignaler *sig); - bool removeSignaler(ChangeSignaler *sig); - }; - - class AGENT_LIB_API ChangeSignaler + return true; + } + + /// @brief single all waiting observers if this sequence number is greater than the last + /// + /// also cancel the timer + /// @param[in] sequence the sequence number of the observation + void signal(uint64_t sequence) + { + std::lock_guard scopedLock(m_mutex); + + if (m_sequence > sequence && sequence) + m_sequence = sequence; + + m_timer.cancel(); + } + /// @brief get the last sequence number signaled + /// @return the sequence number + uint64_t getSequence() const { return m_sequence; } + /// @brief check if signaled + /// @return `true` if it was signaled + bool wasSignaled() const { return m_sequence != UINT64_MAX; } + /// @brief reset the signaled state + void reset() { - public: - // Observer Management - void addObserver(ChangeObserver *observer); - bool removeObserver(ChangeObserver *observer); - bool hasObserver(ChangeObserver *observer) const; - void signalObservers(uint64_t sequence) const; - - virtual ~ChangeSignaler(); - - protected: - // Observer Lists - mutable std::recursive_mutex m_observerMutex; - std::list m_observers; - }; - } // namespace observation -} // namespace mtconnect + std::lock_guard scopedLock(m_mutex); + m_sequence = UINT64_MAX; + } + + private: + boost::asio::io_context::strand &m_strand; + mutable std::recursive_mutex m_mutex; + boost::asio::steady_timer m_timer; + + std::list m_signalers; + volatile uint64_t m_sequence = UINT64_MAX; + + protected: + friend class ChangeSignaler; + void addSignaler(ChangeSignaler *sig); + bool removeSignaler(ChangeSignaler *sig); + }; + + /// @brief A signaler of waiting observers + class AGENT_LIB_API ChangeSignaler + { + public: + /// @brief add an observer to the list + /// @param[in] observer an observer + void addObserver(ChangeObserver *observer); + /// @brief remove an observer + /// @param[in] observer an observer + /// @return `true` if the observer was removed + bool removeObserver(ChangeObserver *observer); + /// @brief check if an observer is in the list + /// @param[in] observer an observer + /// @return `true` if the observer is in the list + bool hasObserver(ChangeObserver *observer) const; + /// @brief singal observers with a sequence number + /// @param[in] sequence the sequence number + void signalObservers(uint64_t sequence) const; + + virtual ~ChangeSignaler(); + + protected: + // Observer Lists + mutable std::recursive_mutex m_observerMutex; + std::list m_observers; + }; +} // namespace mtconnect::observation diff --git a/src/mtconnect/observation/observation.hpp b/src/mtconnect/observation/observation.hpp index b730e94a..3fd8c27b 100644 --- a/src/mtconnect/observation/observation.hpp +++ b/src/mtconnect/observation/observation.hpp @@ -31,390 +31,476 @@ #include "mtconnect/entity/entity.hpp" #include "mtconnect/utilities.hpp" -namespace mtconnect { - namespace observation { - // Types of Observations: - // Event, Sample, Timeseries, DataSetEvent, Message, Alarm, - // AssetEvent, ThreeSpaceSmple, Condition, AssetEvent - - class Observation; - using ObservationPtr = std::shared_ptr; - using ObservationList = std::list; - - class AGENT_LIB_API Observation : public entity::Entity +/// @brief Observation namespace +namespace mtconnect::observation { + + class Observation; + using ObservationPtr = std::shared_ptr; + using ObservationList = std::list; + + /// @brief Abstract observation + class AGENT_LIB_API Observation : public entity::Entity + { + public: + using super = entity::Entity; + using entity::Entity::Entity; + + static entity::FactoryPtr getFactory(); + ~Observation() override = default; + virtual ObservationPtr copy() const { return std::make_shared(); } + + /// @brief Method to create an observation for a data item + /// + /// This method should always be used instead of the constructor. + /// + /// @param[in] dataItem related data item + /// @param[in] props properties + /// @param[in] timestamp the timestamp + /// @param[in,out] errors any errors that occurred when creating the observation + /// @return shared pointer to the observations + static ObservationPtr make(const DataItemPtr dataItem, const entity::Properties &props, + const Timestamp ×tamp, entity::ErrorList &errors); + + /// @brief utility method to copy the properties from a data item to a set of properties + /// @param[in] dataItem the data item + /// @param[out] props properties to recieve data item properties + static void setProperties(const DataItemPtr dataItem, entity::Properties &props) { - public: - using super = entity::Entity; - using entity::Entity::Entity; - - static entity::FactoryPtr getFactory(); - ~Observation() override = default; - virtual ObservationPtr copy() const { return std::make_shared(); } - - static ObservationPtr make(const DataItemPtr dataItem, const entity::Properties &props, - const Timestamp ×tamp, entity::ErrorList &errors); - - static void setProperties(const DataItemPtr dataItem, entity::Properties &props) - { - for (auto &prop : dataItem->getObservationProperties()) - props.emplace(prop); - } - - void setDataItem(const DataItemPtr dataItem) - { - m_dataItem = dataItem; - setProperties(dataItem, m_properties); - } - - const auto getDataItem() const { return m_dataItem.lock(); } - auto getSequence() const { return m_sequence; } - - void updateDataItem(std::unordered_map &diMap) - { - auto old = m_dataItem.lock(); - auto ndi = diMap.find(old->getId()); - if (ndi != diMap.end()) - m_dataItem = ndi->second; - else - LOG(trace) << "Observation cannot find data item: " << old->getId(); - } - - void setTimestamp(const Timestamp &ts) - { - m_timestamp = ts; - setProperty("timestamp", m_timestamp); - } - auto getTimestamp() const { return m_timestamp; } - - void setSequence(int64_t sequence) - { - m_sequence = sequence; - setProperty("sequence", sequence); - } + for (auto &prop : dataItem->getObservationProperties()) + props.emplace(prop); + } - virtual void makeUnavailable() - { - using namespace std::literals; - m_unavailable = true; - setProperty("VALUE", "UNAVAILABLE"s); - } - bool isUnavailable() const { return m_unavailable; } - virtual void setEntityName() - { - auto di = m_dataItem.lock(); - if (di) - Entity::setQName(di->getObservationName()); - } + /// @brief set the associated data item and its properties + /// @param[in] dataItem the data item + void setDataItem(const DataItemPtr dataItem) + { + m_dataItem = dataItem; + setProperties(dataItem, m_properties); + } + + /// @brief get the associated data item + /// @return shared pointer to the data item + const auto getDataItem() const { return m_dataItem.lock(); } + /// @brief get the sequence number of the observation + /// @return the sequence number + auto getSequence() const { return m_sequence; } + + /// @brief update related data item when the device is updated + /// @param[in] diMap a map of data item ids to data items + void updateDataItem(std::unordered_map &diMap) + { + auto old = m_dataItem.lock(); + auto ndi = diMap.find(old->getId()); + if (ndi != diMap.end()) + m_dataItem = ndi->second; + else + LOG(trace) << "Observation cannot find data item: " << old->getId(); + } + + /// @brief set the timestamp + /// @param[in] ts the timestamp + void setTimestamp(const Timestamp &ts) + { + m_timestamp = ts; + setProperty("timestamp", m_timestamp); + } + /// @brief get the timestamp + /// @return the timestamp + auto getTimestamp() const { return m_timestamp; } + + /// @brief set the sequence number + /// @param[in] sequence the sequence number + void setSequence(int64_t sequence) + { + m_sequence = sequence; + setProperty("sequence", sequence); + } + /// @brief make the observation unavailable + virtual void makeUnavailable() + { + using namespace std::literals; + m_unavailable = true; + setProperty("VALUE", "UNAVAILABLE"s); + } + /// @brief get the unavailable state + /// @return `true` if unavailable + bool isUnavailable() const { return m_unavailable; } + /// @brief set the entity name (QName) from the data item observation name + virtual void setEntityName() + { + auto di = m_dataItem.lock(); + if (di) + Entity::setQName(di->getObservationName()); + } + /// @brief Compare observations + /// + /// compare by the data item and then by sequence number + /// @param[in] another the other observation + /// @return `true` if this observation is less than `another` + bool operator<(const Observation &another) const + { + auto di = m_dataItem.lock(); + if (!di) + return false; + auto odi = another.m_dataItem.lock(); + if (!odi) + return true; + + if ((*di) < (*odi)) + return true; + else if (*di == *odi) + return m_sequence < another.m_sequence; + else + return false; + } - bool operator<(const Observation &another) const + /// @brief check if the data item has been removed when device is updated + /// @return `true` if the observation is no longer viable + bool isOrphan() const + { +#ifdef NDEBUG + return m_dataItem.expired(); +#else + if (m_dataItem.expired()) + return true; + if (m_dataItem.lock()->isOrphan()) { auto di = m_dataItem.lock(); - if (!di) - return false; - auto odi = another.m_dataItem.lock(); - if (!odi) - return true; - - if ((*di) < (*odi)) - return true; - else if (*di == *odi) - return m_sequence < another.m_sequence; - else - return false; + LOG(trace) << "!!! DataItem " << di->getTopicName() << " orphaned"; + return true; } - - bool isOrphan() const - { -#ifdef NDEBUG - return m_dataItem.expired(); -#else - if (m_dataItem.expired()) - return true; - if (m_dataItem.lock()->isOrphan()) - { - auto di = m_dataItem.lock(); - LOG(trace) << "!!! DataItem " << di->getTopicName() << " orphaned"; - return true; - } - return false; + return false; #endif - } - - void clearResetTriggered() { m_properties.erase("resetTriggered"); } - - protected: - Timestamp m_timestamp; - bool m_unavailable {false}; - std::weak_ptr m_dataItem; - uint64_t m_sequence {0}; - }; - - class AGENT_LIB_API Sample : public Observation + } + + /// @brief Clear the reset triggered state + void clearResetTriggered() { m_properties.erase("resetTriggered"); } + + protected: + Timestamp m_timestamp; + bool m_unavailable {false}; + std::weak_ptr m_dataItem; + uint64_t m_sequence {0}; + }; + + /// @brief A MTConnect Sample with a double value + class AGENT_LIB_API Sample : public Observation + { + public: + using super = Observation; + + using Observation::Observation; + static entity::FactoryPtr getFactory(); + ~Sample() override = default; + + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + /// @brief An MTConnect Sample with a Vector with three values for X, Y and Z, or A, B, and C. + class AGENT_LIB_API ThreeSpaceSample : public Sample + { + public: + using super = Sample; + + using Sample::Sample; + static entity::FactoryPtr getFactory(); + ~ThreeSpaceSample() override = default; + }; + + /// @brief A vector of timeseries values with a count and duration + class AGENT_LIB_API Timeseries : public Sample + { + public: + using super = Sample; + + using Sample::Sample; + static entity::FactoryPtr getFactory(); + ~Timeseries() override = default; + + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + class Condition; + using ConditionPtr = std::shared_ptr; + using ConditionList = std::list; + + /// @brief An MTConnect Condition + /// + /// Conditions are linked together to keep track of all the conditions that are + /// active at one time. When the normal condition arrives, the list is cleared. + class AGENT_LIB_API Condition : public Observation + { + public: + using super = Observation; + + /// @brief The Condition level + enum Level { - public: - using super = Observation; - - using Observation::Observation; - static entity::FactoryPtr getFactory(); - ~Sample() override = default; - - ObservationPtr copy() const override { return std::make_shared(*this); } + NORMAL, + WARNING, + FAULT, + UNAVAILABLE }; - class AGENT_LIB_API ThreeSpaceSample : public Sample - { - public: - using super = Sample; + using Observation::Observation; + static entity::FactoryPtr getFactory(); + ~Condition() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } - using Sample::Sample; - static entity::FactoryPtr getFactory(); - ~ThreeSpaceSample() override = default; - }; + ConditionPtr getptr() { return std::dynamic_pointer_cast(Entity::getptr()); } - class AGENT_LIB_API Timeseries : public Sample + /// @brief Set the conditions level + /// @param[in] level the level + void setLevel(Level level) { - public: - using super = Sample; + m_level = level; + setEntityName(); + } - using Sample::Sample; - static entity::FactoryPtr getFactory(); - ~Timeseries() override = default; - - ObservationPtr copy() const override { return std::make_shared(*this); } - }; - - class Condition; - using ConditionPtr = std::shared_ptr; - using ConditionList = std::list; - - class AGENT_LIB_API Condition : public Observation + /// @brief set the level as a string + /// @param[in] s the level + void setLevel(const std::string &s) { - public: - using super = Observation; - - enum Level - { - NORMAL, - WARNING, - FAULT, - UNAVAILABLE - }; - - using Observation::Observation; - static entity::FactoryPtr getFactory(); - ~Condition() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - - ConditionPtr getptr() { return std::dynamic_pointer_cast(Entity::getptr()); } - - void setLevel(Level level) - { - m_level = level; - setEntityName(); - } - - void setLevel(const std::string &s) - { - if (iequals("normal", s)) - setLevel(NORMAL); - else if (iequals("warning", s)) - setLevel(WARNING); - else if (iequals("fault", s)) - setLevel(FAULT); - else if (iequals("unavailable", s)) - setLevel(UNAVAILABLE); - else - throw entity::PropertyError("Invalid Condition Level: " + s); - } - - void normal() - { - m_level = NORMAL; - m_code.clear(); - m_properties.erase("nativeCode"); - m_properties.erase("nativeSeverity"); - m_properties.erase("qualifier"); - m_properties.erase("statistic"); - m_properties.erase("VALUE"); - setEntityName(); - } - - void makeUnavailable() override - { - m_unavailable = true; - m_level = UNAVAILABLE; - setEntityName(); - } - - void setEntityName() override - { - switch (m_level) - { - case NORMAL: - setQName("Normal"); - break; - - case WARNING: - setQName("Warning"); - break; - - case FAULT: - setQName("Fault"); - break; - - case UNAVAILABLE: - setQName("Unavailable"); - break; - } - } - - ConditionPtr getFirst() + if (iequals("normal", s)) + setLevel(NORMAL); + else if (iequals("warning", s)) + setLevel(WARNING); + else if (iequals("fault", s)) + setLevel(FAULT); + else if (iequals("unavailable", s)) + setLevel(UNAVAILABLE); + else + throw entity::PropertyError("Invalid Condition Level: " + s); + } + + /// @brief Make this condition normal + void normal() + { + m_level = NORMAL; + m_code.clear(); + m_properties.erase("nativeCode"); + m_properties.erase("nativeSeverity"); + m_properties.erase("qualifier"); + m_properties.erase("statistic"); + m_properties.erase("VALUE"); + setEntityName(); + } + /// @brief Make this condition unavailable + void makeUnavailable() override + { + m_unavailable = true; + m_level = UNAVAILABLE; + setEntityName(); + } + /// @brief Using the level, set the QName of this Observation + void setEntityName() override + { + switch (m_level) { - if (m_prev) - return m_prev->getFirst(); + case NORMAL: + setQName("Normal"); + break; - return getptr(); - } + case WARNING: + setQName("Warning"); + break; - void getConditionList(ConditionList &list) - { - if (m_prev) - m_prev->getConditionList(list); + case FAULT: + setQName("Fault"); + break; - list.emplace_back(getptr()); + case UNAVAILABLE: + setQName("Unavailable"); + break; } + } - ConditionPtr find(const std::string &code) - { - if (m_code == code) - return getptr(); - - if (m_prev) - return m_prev->find(code); - - return nullptr; - } - - bool replace(ConditionPtr &old, ConditionPtr &_new); - ConditionPtr deepCopy(); - ConditionPtr deepCopyAndRemove(ConditionPtr &old); - - const std::string &getCode() const { return m_code; } - Level getLevel() const { return m_level; } - ConditionPtr getPrev() const { return m_prev; } - void appendTo(ConditionPtr cond) { m_prev = cond; } - - protected: - std::string m_code; - Level m_level {NORMAL}; - ConditionPtr m_prev; - }; - - class AGENT_LIB_API Event : public Observation + /// @brief get the first condition in the chain + /// + /// Conditions are chained together to allow for mutiple conditions active at the same time + /// @return shared pointer the first condition in the chain + ConditionPtr getFirst() { - public: - using super = Observation; + if (m_prev) + return m_prev->getFirst(); - using Observation::Observation; - static entity::FactoryPtr getFactory(); - ~Event() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - }; - - class AGENT_LIB_API DoubleEvent : public Observation - { - public: - using super = Observation; - - using Observation::Observation; - static entity::FactoryPtr getFactory(); - ~DoubleEvent() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - }; + return getptr(); + } - class AGENT_LIB_API IntEvent : public Observation + /// @brief Get a list of all active conditions + /// @param[out] list the list condtions + void getConditionList(ConditionList &list) { - public: - using super = Observation; - - using Observation::Observation; - static entity::FactoryPtr getFactory(); - ~IntEvent() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - }; - - class AGENT_LIB_API DataSetEvent : public Event - { - public: - using super = Event; - - using Event::Event; - static entity::FactoryPtr getFactory(); - ~DataSetEvent() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - - void makeUnavailable() override - { - super::makeUnavailable(); - setProperty("count", int64_t(0)); - } - - const entity::DataSet &getDataSet() const - { - const entity::Value &v = getValue(); - return std::get(v); - } - void setDataSet(const entity::DataSet &set) - { - setValue(set); - setProperty("count", int64_t(set.size())); - } - }; + if (m_prev) + m_prev->getConditionList(list); - using DataSetEventPtr = std::shared_ptr; + list.emplace_back(getptr()); + } - class AGENT_LIB_API TableEvent : public DataSetEvent + /// @brief find a condition by code in the condition list + /// @param[in] code te code + /// @return shared pointer to the condition if found + ConditionPtr find(const std::string &code) { - public: - using DataSetEvent::DataSetEvent; - static entity::FactoryPtr getFactory(); - ObservationPtr copy() const override { return std::make_shared(*this); } - }; + if (m_code == code) + return getptr(); - class AGENT_LIB_API AssetEvent : public Event + if (m_prev) + return m_prev->find(code); + + return nullptr; + } + + /// @brief replace a condition with another in the condition list + /// @param[in] old the condition to be placed + /// @param[in] _new the replacement condition + /// @return `true` if the old condition was found + bool replace(ConditionPtr &old, ConditionPtr &_new); + /// @brief copy the condition and all conditions in the list + /// @return a new shared condition pointer + ConditionPtr deepCopy(); + /// @brief copy the condition and all conditions in the list removing one condition + /// @param[in] old the condition to skip + /// @return the new condition pointer + ConditionPtr deepCopyAndRemove(ConditionPtr &old); + + /// @brief Get the code for the condition + /// @return the code + const std::string &getCode() const { return m_code; } + /// @brief get the condition level + /// @return the level + Level getLevel() const { return m_level; } + /// @brief get the previous condition in the list + /// @return the previous condition if it exists + ConditionPtr getPrev() const { return m_prev; } + /// @brief make a condition as the previous condition linking it to the list + /// @param[in] cond the previous condition + void appendTo(ConditionPtr cond) { m_prev = cond; } + + protected: + std::string m_code; + Level m_level {NORMAL}; + ConditionPtr m_prev; + }; + + /// @brief an MTConnect Event with a string value or controlled vocabulary + class AGENT_LIB_API Event : public Observation + { + public: + using super = Observation; + + using Observation::Observation; + static entity::FactoryPtr getFactory(); + ~Event() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + /// @brief An `Event` that has a double value + class AGENT_LIB_API DoubleEvent : public Observation + { + public: + using super = Observation; + + using Observation::Observation; + static entity::FactoryPtr getFactory(); + ~DoubleEvent() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + /// @brief An `Event` that has a integer value + class AGENT_LIB_API IntEvent : public Observation + { + public: + using super = Observation; + + using Observation::Observation; + static entity::FactoryPtr getFactory(); + ~IntEvent() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + /// @brief An `Event` that has a data set representation + class AGENT_LIB_API DataSetEvent : public Event + { + public: + using super = Event; + + using Event::Event; + static entity::FactoryPtr getFactory(); + ~DataSetEvent() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } + + /// @brief makes the data set unavailable and sets the count to 0 + void makeUnavailable() override { - public: - using Event::Event; - static entity::FactoryPtr getFactory(); - ~AssetEvent() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - - protected: - }; - - class AGENT_LIB_API Message : public Event + super::makeUnavailable(); + setProperty("count", int64_t(0)); + } + /// @brief get the data set value + /// @return the value + const entity::DataSet &getDataSet() const { - public: - using super = Event; - - using Event::Event; - static entity::FactoryPtr getFactory(); - ~Message() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - }; - - class AGENT_LIB_API Alarm : public Event + const entity::Value &v = getValue(); + return std::get(v); + } + /// @brief set the data set value and the count + /// @param[in] set the data set + void setDataSet(const entity::DataSet &set) { - public: - using super = Event; - - using Event::Event; - static entity::FactoryPtr getFactory(); - ~Alarm() override = default; - ObservationPtr copy() const override { return std::make_shared(*this); } - }; - - using ObservationComparer = bool (*)(ObservationPtr &, ObservationPtr &); - inline bool ObservationCompare(ObservationPtr &aE1, ObservationPtr &aE2) { return *aE1 < *aE2; } - } // namespace observation -} // namespace mtconnect + setValue(set); + setProperty("count", int64_t(set.size())); + } + }; + + using DataSetEventPtr = std::shared_ptr; + + /// @brief An `Event` that has a table representation + class AGENT_LIB_API TableEvent : public DataSetEvent + { + public: + using DataSetEvent::DataSetEvent; + static entity::FactoryPtr getFactory(); + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + /// @brief An asset changed or removed Event + class AGENT_LIB_API AssetEvent : public Event + { + public: + using Event::Event; + static entity::FactoryPtr getFactory(); + ~AssetEvent() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } + + protected: + }; + + /// @brief A Message Event + class AGENT_LIB_API Message : public Event + { + public: + using super = Event; + + using Event::Event; + static entity::FactoryPtr getFactory(); + ~Message() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + /// @brief A deprecated Alarm type. + /// + /// Alarms should not be used in modern MTConnect + class AGENT_LIB_API Alarm : public Event + { + public: + using super = Event; + + using Event::Event; + static entity::FactoryPtr getFactory(); + ~Alarm() override = default; + ObservationPtr copy() const override { return std::make_shared(*this); } + }; + + using ObservationComparer = bool (*)(ObservationPtr &, ObservationPtr &); + inline bool ObservationCompare(ObservationPtr &aE1, ObservationPtr &aE2) { return *aE1 < *aE2; } +} // namespace mtconnect::observation diff --git a/src/mtconnect/parser/xml_parser.hpp b/src/mtconnect/parser/xml_parser.hpp index 5c0edcf6..6590d708 100644 --- a/src/mtconnect/parser/xml_parser.hpp +++ b/src/mtconnect/parser/xml_parser.hpp @@ -34,25 +34,34 @@ namespace mtconnect::printer { class XmlPrinter; } -namespace mtconnect::parser { +/// @brief MTConnect Device parser namespace +namespace mtconnect::parser { + /// @brief parse an xml document and create a list of devices class AGENT_LIB_API XmlParser { public: - // Constructor to set the open the correct file + /// @brief Constructor to set the open the correct file XmlParser(); virtual ~XmlParser(); - // Parses a file and returns a list of devices + /// @brief Parses a file and returns a list of devices + /// @param[in] aPath to the file + /// @param[in] aPrinter the printer to obtain and set namespaces std::list parseFile(const std::string &aPath, printer::XmlPrinter *aPrinter); - // Just loads the document, assumed it has already been parsed before. + /// @brief Just loads the document, assumed it has already been parsed before. + /// @param aDoc the XML document to parse void loadDocument(const std::string &aDoc); - - // Get std::list of data items in path + /// @brief get data items given a filter set and an xpath + /// @param[out] filterSet a filter set to build + /// @param[in] path the xpath + /// @param[in] node an option node pointer to start from. defaults to the document root. void getDataItems(FilterSet &filterSet, const std::string &path, xmlNodePtr node = nullptr); + /// @brief get the schema version + /// @return the version const auto &getSchemaVersion() const { return m_schemaVersion; } protected: diff --git a/src/mtconnect/pipeline/convert_sample.hpp b/src/mtconnect/pipeline/convert_sample.hpp index 4c920575..82f9415a 100644 --- a/src/mtconnect/pipeline/convert_sample.hpp +++ b/src/mtconnect/pipeline/convert_sample.hpp @@ -22,33 +22,32 @@ #include "mtconnect/observation/observation.hpp" #include "transform.hpp" -namespace mtconnect { - namespace pipeline { - class AGENT_LIB_API ConvertSample : public Transform +namespace mtconnect::pipeline { + /// @brief Transform that converts units if necessary + class AGENT_LIB_API ConvertSample : public Transform + { + public: + ConvertSample() : Transform("ConvertSample") { - public: - ConvertSample() : Transform("ConvertSample") - { - using namespace observation; - m_guard = TypeGuard(RUN) || TypeGuard(SKIP); - } - const entity::EntityPtr operator()(const entity::EntityPtr entity) override + using namespace observation; + m_guard = TypeGuard(RUN) || TypeGuard(SKIP); + } + const entity::EntityPtr operator()(const entity::EntityPtr entity) override + { + using namespace observation; + using namespace entity; + auto sample = std::dynamic_pointer_cast(entity); + if (sample && !sample->isOrphan() && !sample->isUnavailable()) { - using namespace observation; - using namespace entity; - auto sample = std::dynamic_pointer_cast(entity); - if (sample && !sample->isOrphan() && !sample->isUnavailable()) + auto &converter = sample->getDataItem()->getConverter(); + if (converter) { - auto &converter = sample->getDataItem()->getConverter(); - if (converter) - { - auto ns = sample->copy(); - converter->convertValue(ns->getValue()); - return next(ns); - } + auto ns = sample->copy(); + converter->convertValue(ns->getValue()); + return next(ns); } - return next(entity); } - }; - } // namespace pipeline -} // namespace mtconnect + return next(entity); + } + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/deliver.hpp b/src/mtconnect/pipeline/deliver.hpp index 4724e298..cca9d3ee 100644 --- a/src/mtconnect/pipeline/deliver.hpp +++ b/src/mtconnect/pipeline/deliver.hpp @@ -26,166 +26,185 @@ #include "mtconnect/observation/observation.hpp" #include "transform.hpp" -namespace mtconnect { - namespace pipeline { - struct ComputeMetrics : std::enable_shared_from_this +namespace mtconnect::pipeline { + /// @brief Utility class to periodically compute metrics + struct ComputeMetrics : std::enable_shared_from_this + { + /// @brief Construct a metrics stats computer + /// @param st the strand for async operations + /// @param contract contract to use + /// @param dataItem the data item to post the metrics + /// @param count a shared count + ComputeMetrics(boost::asio::io_context::strand &st, PipelineContract *contract, + const std::optional &dataItem, std::shared_ptr &count) + : m_count(count), + m_contract(contract), + m_dataItem(dataItem), + m_strand(st), + m_timer(st.context()) + {} + + std::shared_ptr ptr() { return shared_from_this(); } + + /// @brief the compute method that get called periodically + /// @param ec an error code from the timer + void compute(boost::system::error_code ec); + + /// @brief stop the timer and cancel computation + void stop() { - ComputeMetrics(boost::asio::io_context::strand &st, PipelineContract *contract, - const std::optional &dataItem, std::shared_ptr &count) - : m_count(count), - m_contract(contract), - m_dataItem(dataItem), - m_strand(st), - m_timer(st.context()) - {} - - std::shared_ptr ptr() { return shared_from_this(); } - - void compute(boost::system::error_code ec); - - void stop() - { - m_stopped = true; - m_timer.cancel(); - } - - void start(); - - std::shared_ptr m_count; - PipelineContract *m_contract {nullptr}; - std::optional m_dataItem; - std::chrono::time_point m_lastTime; - - boost::asio::io_context::strand &m_strand; - boost::asio::steady_timer m_timer; - bool m_first {true}; - bool m_stopped {false}; - size_t m_last {0}; - double m_lastAvg {0.0}; - }; - - class AGENT_LIB_API MeteredTransform : public Transform + m_stopped = true; + m_timer.cancel(); + } + + /// @brief start computation + void start(); + + std::shared_ptr m_count; + PipelineContract *m_contract {nullptr}; + std::optional m_dataItem; + std::chrono::time_point m_lastTime; + + boost::asio::io_context::strand &m_strand; + boost::asio::steady_timer m_timer; + bool m_first {true}; + bool m_stopped {false}; + size_t m_last {0}; + double m_lastAvg {0.0}; + }; + + /// @brief A transform that computes metrics using `ComputeMetrics` + class AGENT_LIB_API MeteredTransform : public Transform + { + public: + /// @brief Construct an abstract metered transform + /// @param name the name of the transform from the subclass + /// @param context the pipeline context + /// @param metricsDataItem the data item used for an observation when the metrics are updated + MeteredTransform(const std::string &name, PipelineContextPtr context, + const std::optional &metricsDataItem = std::nullopt) + : Transform(name), + m_contract(context->m_contract.get()), + m_count(std::make_shared(0)), + m_dataItem(metricsDataItem) + {} + + ~MeteredTransform() override { - public: - MeteredTransform(const std::string &name, PipelineContextPtr context, - const std::optional &metricsDataItem = std::nullopt) - : Transform(name), - m_contract(context->m_contract.get()), - m_count(std::make_shared(0)), - m_dataItem(metricsDataItem) - {} - - ~MeteredTransform() override - { - if (m_metrics) - m_metrics->stop(); - } - - void stop() override - { - if (m_metrics) - m_metrics->stop(); - - Transform::stop(); - } - - void start(boost::asio::io_context::strand &st) override - { - if (m_dataItem) - { - m_metrics = std::make_shared(st, m_contract, m_dataItem, m_count); - m_metrics->start(); - } - Transform::start(st); - } + if (m_metrics) + m_metrics->stop(); + } - protected: - friend struct ComputeMetrics; + /// @brief Stop the metrics + void stop() override + { + if (m_metrics) + m_metrics->stop(); - PipelineContract *m_contract; - std::shared_ptr m_count; - std::shared_ptr m_metrics; - std::optional m_dataItem; - }; + Transform::stop(); + } - class AGENT_LIB_API DeliverObservation : public MeteredTransform + /// @brief start the metrics + /// @param st the context to post observations + void start(boost::asio::io_context::strand &st) override { - public: - using Deliver = std::function; - DeliverObservation(PipelineContextPtr context, - const std::optional &metricDataItem = std::nullopt) - : MeteredTransform("DeliverObservation", context, metricDataItem) + if (m_dataItem) { - m_guard = TypeGuard(RUN); + m_metrics = std::make_shared(st, m_contract, m_dataItem, m_count); + m_metrics->start(); } - const entity::EntityPtr operator()(const entity::EntityPtr entity) override; - }; - - class AGENT_LIB_API DeliverAsset : public MeteredTransform + Transform::start(st); + } + + protected: + friend struct ComputeMetrics; + + PipelineContract *m_contract; + std::shared_ptr m_count; + std::shared_ptr m_metrics; + std::optional m_dataItem; + }; + + /// @brief A transform to deliver and meter observation delivery + class AGENT_LIB_API DeliverObservation : public MeteredTransform + { + public: + using Deliver = std::function; + DeliverObservation(PipelineContextPtr context, + const std::optional &metricDataItem = std::nullopt) + : MeteredTransform("DeliverObservation", context, metricDataItem) { - public: - using Deliver = std::function; - DeliverAsset(PipelineContextPtr context, - const std::optional &metricsDataItem = std::nullopt) - : MeteredTransform("DeliverAsset", context, metricsDataItem) - { - m_guard = TypeGuard(RUN); - } - const entity::EntityPtr operator()(const entity::EntityPtr entity) override; - }; - - class AGENT_LIB_API DeliverConnectionStatus : public Transform + m_guard = TypeGuard(RUN); + } + const entity::EntityPtr operator()(const entity::EntityPtr entity) override; + }; + + /// @brief A transform to deliver and meter asset delivery + class AGENT_LIB_API DeliverAsset : public MeteredTransform + { + public: + using Deliver = std::function; + DeliverAsset(PipelineContextPtr context, + const std::optional &metricsDataItem = std::nullopt) + : MeteredTransform("DeliverAsset", context, metricsDataItem) { - public: - using Deliver = std::function; - DeliverConnectionStatus(PipelineContextPtr context, const StringList &devices, - bool autoAvailable) - : Transform("DeliverConnectionStatus"), - m_contract(context->m_contract.get()), - m_devices(devices), - m_autoAvailable(autoAvailable) - { - m_guard = EntityNameGuard("ConnectionStatus", RUN); - } - const entity::EntityPtr operator()(const entity::EntityPtr entity) override; - - protected: - PipelineContract *m_contract; - std::list m_devices; - bool m_autoAvailable; - }; - - class AGENT_LIB_API DeliverAssetCommand : public Transform + m_guard = TypeGuard(RUN); + } + const entity::EntityPtr operator()(const entity::EntityPtr entity) override; + }; + + /// @brief deliver the connection status of an adapter + class AGENT_LIB_API DeliverConnectionStatus : public Transform + { + public: + using Deliver = std::function; + DeliverConnectionStatus(PipelineContextPtr context, const StringList &devices, + bool autoAvailable) + : Transform("DeliverConnectionStatus"), + m_contract(context->m_contract.get()), + m_devices(devices), + m_autoAvailable(autoAvailable) { - public: - using Deliver = std::function; - DeliverAssetCommand(PipelineContextPtr context) - : Transform("DeliverAssetCommand"), m_contract(context->m_contract.get()) - { - m_guard = EntityNameGuard("AssetCommand", RUN); - } - const entity::EntityPtr operator()(const entity::EntityPtr entity) override; - - protected: - PipelineContract *m_contract; - }; - - class AGENT_LIB_API DeliverCommand : public Transform + m_guard = EntityNameGuard("ConnectionStatus", RUN); + } + const entity::EntityPtr operator()(const entity::EntityPtr entity) override; + + protected: + PipelineContract *m_contract; + std::list m_devices; + bool m_autoAvailable; + }; + + /// @brief Deliver an asset command + class AGENT_LIB_API DeliverAssetCommand : public Transform + { + public: + using Deliver = std::function; + DeliverAssetCommand(PipelineContextPtr context) + : Transform("DeliverAssetCommand"), m_contract(context->m_contract.get()) { - public: - using Deliver = std::function; - DeliverCommand(PipelineContextPtr context, const std::optional &device) - : Transform("DeliverCommand"), - m_contract(context->m_contract.get()), - m_defaultDevice(device) - { - m_guard = EntityNameGuard("Command", RUN); - } - const entity::EntityPtr operator()(const entity::EntityPtr entity) override; - - protected: - PipelineContract *m_contract; - std::optional m_defaultDevice; - }; - } // namespace pipeline -} // namespace mtconnect + m_guard = EntityNameGuard("AssetCommand", RUN); + } + const entity::EntityPtr operator()(const entity::EntityPtr entity) override; + + protected: + PipelineContract *m_contract; + }; + + /// @brief Deliver an adapter command + class AGENT_LIB_API DeliverCommand : public Transform + { + public: + using Deliver = std::function; + DeliverCommand(PipelineContextPtr context, const std::optional &device) + : Transform("DeliverCommand"), m_contract(context->m_contract.get()), m_defaultDevice(device) + { + m_guard = EntityNameGuard("Command", RUN); + } + const entity::EntityPtr operator()(const entity::EntityPtr entity) override; + + protected: + PipelineContract *m_contract; + std::optional m_defaultDevice; + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/delta_filter.hpp b/src/mtconnect/pipeline/delta_filter.hpp index 860e9d96..f1d8f968 100644 --- a/src/mtconnect/pipeline/delta_filter.hpp +++ b/src/mtconnect/pipeline/delta_filter.hpp @@ -24,16 +24,20 @@ namespace mtconnect { class Agent; namespace pipeline { + /// @brief Provide MTConnect DataItem delta filter behavior class AGENT_LIB_API DeltaFilter : public Transform { public: + /// @brief shared values associated with data items struct State : TransformState { std::unordered_map m_lastSampleValue; }; + /// @brief Construct a delta filter + /// @param[in] context the context for shared state DeltaFilter(PipelineContextPtr context) - : Transform("RateFilter"), + : Transform("DeltaFilter"), m_state(context->getSharedState(m_name)), m_contract(context->m_contract.get()) { diff --git a/src/mtconnect/pipeline/duplicate_filter.hpp b/src/mtconnect/pipeline/duplicate_filter.hpp index f2f01fb9..2b7540c6 100644 --- a/src/mtconnect/pipeline/duplicate_filter.hpp +++ b/src/mtconnect/pipeline/duplicate_filter.hpp @@ -20,60 +20,59 @@ #include "mtconnect/config.hpp" #include "transform.hpp" -namespace mtconnect { - namespace pipeline { - class AGENT_LIB_API DuplicateFilter : public Transform +namespace mtconnect::pipeline { + /// @brief Filter duplicates + class AGENT_LIB_API DuplicateFilter : public Transform + { + public: + /// @brief Shared states to check for duplicates + struct State : TransformState { - public: - struct State : TransformState - { - std::unordered_map m_values; - }; - - DuplicateFilter(const DuplicateFilter &) = default; - DuplicateFilter(PipelineContextPtr context) - : Transform("DuplicateFilter"), m_state(context->getSharedState(m_name)) - { - using namespace observation; - static constexpr auto lambda = [](const Observation &o) { - return !o.isOrphan() && !o.getDataItem()->isDiscrete(); - }; - m_guard = - LambdaGuard>( - lambda, RUN) || - TypeGuard(SKIP); - } - ~DuplicateFilter() override = default; - - const entity::EntityPtr operator()(const entity::EntityPtr entity) override - { - using namespace observation; - std::lock_guard guard(*m_state); + std::unordered_map m_values; + }; - auto o = std::dynamic_pointer_cast(entity); - if (o->isOrphan()) - return entity::EntityPtr(); + DuplicateFilter(const DuplicateFilter &) = default; + /// @brief Create a duplicate filter with shared state from the context + /// @param context the context + DuplicateFilter(PipelineContextPtr context) + : Transform("DuplicateFilter"), m_state(context->getSharedState(m_name)) + { + using namespace observation; + static constexpr auto lambda = [](const Observation &o) { + return !o.isOrphan() && !o.getDataItem()->isDiscrete(); + }; + m_guard = LambdaGuard>( + lambda, RUN) || + TypeGuard(SKIP); + } + ~DuplicateFilter() override = default; - auto di = o->getDataItem(); - auto &id = di->getId(); + const entity::EntityPtr operator()(const entity::EntityPtr entity) override + { + using namespace observation; + std::lock_guard guard(*m_state); - auto &values = m_state->m_values; - auto old = values.find(id); - if (old != values.end() && old->second == o->getValue()) - return entity::EntityPtr(); + auto o = std::dynamic_pointer_cast(entity); + if (o->isOrphan()) + return entity::EntityPtr(); - if (old == values.end()) - values[id] = o->getValue(); - else - old->second = o->getValue(); + auto di = o->getDataItem(); + auto &id = di->getId(); - return next(entity); - } + auto &values = m_state->m_values; + auto old = values.find(id); + if (old != values.end() && old->second == o->getValue()) + return entity::EntityPtr(); - protected: - std::shared_ptr m_state; - }; + if (old == values.end()) + values[id] = o->getValue(); + else + old->second = o->getValue(); - } // namespace pipeline + return next(entity); + } -} // namespace mtconnect + protected: + std::shared_ptr m_state; + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/guard.hpp b/src/mtconnect/pipeline/guard.hpp index a5595523..f9589552 100644 --- a/src/mtconnect/pipeline/guard.hpp +++ b/src/mtconnect/pipeline/guard.hpp @@ -22,24 +22,38 @@ namespace mtconnect { namespace pipeline { + /// @brief Actions for taken for the guard enum GuardAction { - CONTINUE, - RUN, - SKIP + CONTINUE, ///< Continue on to the next transform in the list + RUN, ///< Run this transform + SKIP ///< Skip the transform and move to the next }; + /// @brief Guard is a lambda function returning a `GuardAction` taking an entity using Guard = std::function; + + /// @brief A simple GuardClass returning a simple match + /// + /// allows for chaining of guards class GuardCls { public: + /// @brief Construct a GuardCls + /// @param match the match to return if matched GuardCls(GuardAction match) : m_match(match) {} GuardCls(const GuardCls &) = default; GuardAction operator()(const entity::EntityPtr entity) { return m_match; } + /// @brief set the alternative guard + /// @param alt alternative void setAlternative(Guard &alt) { m_alternative = alt; } + /// @brief check the matched state and if matched then return action. + /// @param matched if `true` return the action otherwise check an alternative + /// @param entity an entity + /// @return the guard action GuardAction check(bool matched, const entity::EntityPtr entity) { if (matched) @@ -50,12 +64,17 @@ namespace mtconnect { return CONTINUE; } + /// @brief set the alternative to the other + /// @param other a guard + /// @return this auto &operator||(Guard other) { m_alternative = other; return *this; } - + /// @brief Set the alternative to a static action + /// @param other the guard action + /// @return this auto &operator||(GuardAction other) { m_alternative = GuardCls(other); @@ -67,12 +86,21 @@ namespace mtconnect { GuardAction m_match; }; + /// @brief A guard that checks if the entity is one of the types or sub-types + /// @tparam ...Ts the list of types template class TypeGuard : public GuardCls { public: using GuardCls::GuardCls; + /// @brief recursive match + /// + /// Uses dynamic cast to check if entity can be cast as one of the types + /// @tparam T the type + /// @tparam ...R the rest of the types + /// @param ti the type info we're checking + /// @return `true` if matches template constexpr bool match(const entity::Entity *ep) { @@ -82,6 +110,9 @@ namespace mtconnect { return dynamic_cast(ep) != nullptr || match(ep); } + /// @brief constexpr expanded type match + /// @param entity the entity + /// @return `true` if matches constexpr bool matches(const entity::EntityPtr &entity) { return match(entity.get()); } GuardAction operator()(const entity::EntityPtr entity) @@ -96,12 +127,19 @@ namespace mtconnect { } }; + /// @brief A guard that checks if the entity that matches one of the types + /// @tparam ...Ts the list of types template class ExactTypeGuard : public GuardCls { public: using GuardCls::GuardCls; + /// @brief recursive match + /// @tparam T the type + /// @tparam ...R the rest of the types + /// @param ti the type info we're checking + /// @return `true` if matches template constexpr bool match(const std::type_info &ti) { @@ -111,6 +149,9 @@ namespace mtconnect { return typeid(T) == ti || match(ti); } + /// @brief constexpr expanded type match + /// @param entity the entity + /// @return `true` if matches constexpr bool matches(const entity::EntityPtr &entity) { auto &e = *entity.get(); @@ -129,6 +170,7 @@ namespace mtconnect { } }; + /// @brief Match on the entity name class EntityNameGuard : public GuardCls { public: @@ -150,16 +192,25 @@ namespace mtconnect { std::string m_name; }; + /// @brief Use a lambda expression to match lambda + /// @tparam L lamba argument type + /// @tparam B class with a matches method to match the entity template class LambdaGuard : public B { public: using Lambda = std::function; + /// @brief Construct a lambda guard with a function returning bool and an action + /// @param guard the lambda function + /// @param match the action if the lambda returns true LambdaGuard(Lambda guard, GuardAction match) : B(match), m_lambda(guard) {} LambdaGuard(const LambdaGuard &) = default; ~LambdaGuard() = default; + /// @brief call the `B::matches()` method with the entity + /// @param entity the entity + /// @return `true` if matched bool matches(const entity::EntityPtr &entity) { bool matched = B::matches(entity); diff --git a/src/mtconnect/pipeline/message_mapper.hpp b/src/mtconnect/pipeline/message_mapper.hpp index b5b4fddb..15a8e0db 100644 --- a/src/mtconnect/pipeline/message_mapper.hpp +++ b/src/mtconnect/pipeline/message_mapper.hpp @@ -33,91 +33,90 @@ #include "topic_mapper.hpp" #include "transform.hpp" -namespace mtconnect { - class Device; +namespace mtconnect::pipeline { + /// @brief JsonMapper does nothing, it is a placeholder for a json interpreter + class AGENT_LIB_API JsonMapper : public Transform + { + public: + JsonMapper(const JsonMapper &) = default; + JsonMapper(PipelineContextPtr context) : Transform("JsonMapper"), m_context(context) + { + m_guard = TypeGuard(RUN); + } - namespace pipeline { - class AGENT_LIB_API JsonMapper : public Transform + const EntityPtr operator()(const EntityPtr entity) override { - public: - JsonMapper(const JsonMapper &) = default; - JsonMapper(PipelineContextPtr context) : Transform("JsonMapper"), m_context(context) - { - m_guard = TypeGuard(RUN); - } + auto json = std::dynamic_pointer_cast(entity); - const EntityPtr operator()(const EntityPtr entity) override - { - auto json = std::dynamic_pointer_cast(entity); + return nullptr; + } - return nullptr; - } + protected: + PipelineContextPtr m_context; + }; - protected: - PipelineContextPtr m_context; - }; + /// @brief Attempt to find a data item associated with a topic from a pub/sub message + /// system + class AGENT_LIB_API DataMapper : public Transform + { + public: + DataMapper(const DataMapper &) = default; + DataMapper(PipelineContextPtr context, source::adapter::Handler *handler) + : Transform("DataMapper"), m_context(context), m_handler(handler) + { + m_guard = TypeGuard(RUN); + } - class AGENT_LIB_API DataMapper : public Transform + const EntityPtr operator()(const EntityPtr entity) override { - public: - DataMapper(const DataMapper &) = default; - DataMapper(PipelineContextPtr context, source::adapter::Handler *handler) - : Transform("DataMapper"), m_context(context), m_handler(handler) + auto data = std::dynamic_pointer_cast(entity); + if (data->m_dataItem) { - m_guard = TypeGuard(RUN); - } + entity::Properties props {{"VALUE", data->getValue()}}; + entity::ErrorList errors; + try + { + auto obs = observation::Observation::make(data->m_dataItem, props, + std::chrono::system_clock::now(), errors); + if (errors.empty()) + return next(obs); + } + catch (entity::EntityError &e) + { + LOG(error) << "Could not create observation: " << e.what(); + } + for (auto &e : errors) + { + LOG(warning) << "Error while parsing message data: " << e->what(); + } - const EntityPtr operator()(const EntityPtr entity) override + return nullptr; + } + else { - auto data = std::dynamic_pointer_cast(entity); - if (data->m_dataItem) + if (std::holds_alternative(data->getValue())) { - entity::Properties props {{"VALUE", data->getValue()}}; - entity::ErrorList errors; - try - { - auto obs = observation::Observation::make(data->m_dataItem, props, - std::chrono::system_clock::now(), errors); - if (errors.empty()) - return next(obs); - } - catch (entity::EntityError &e) - { - LOG(error) << "Could not create observation: " << e.what(); - } - for (auto &e : errors) - { - LOG(warning) << "Error while parsing message data: " << e->what(); - } - - return nullptr; + // Try processing as shdr data + auto entity = make_shared( + "Data", Properties {{"VALUE", data->getValue()}, {"source", string("")}}); + next(entity); } else { - if (std::holds_alternative(data->getValue())) - { - // Try processing as shdr data - auto entity = make_shared( - "Data", Properties {{"VALUE", data->getValue()}, {"source", string("")}}); - next(entity); - } + std::string msg; + if (auto topic = data->maybeGet("topic"); topic) + msg = *topic; else - { - std::string msg; - if (auto topic = data->maybeGet("topic"); topic) - msg = *topic; - else - msg = "unknown topic"; - LOG(error) << "Cannot find data item for topic: " << msg - << " and data: " << data->getValue(); - } - return nullptr; + msg = "unknown topic"; + LOG(error) << "Cannot find data item for topic: " << msg + << " and data: " << data->getValue(); } + return nullptr; } + } - protected: - PipelineContextPtr m_context; - source::adapter::Handler *m_handler; - }; - } // namespace pipeline -} // namespace mtconnect + protected: + PipelineContextPtr m_context; + source::adapter::Handler *m_handler; + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/mtconnect_xml_transform.hpp b/src/mtconnect/pipeline/mtconnect_xml_transform.hpp index 0eb6d436..94225067 100644 --- a/src/mtconnect/pipeline/mtconnect_xml_transform.hpp +++ b/src/mtconnect/pipeline/mtconnect_xml_transform.hpp @@ -27,6 +27,7 @@ #include "response_document.hpp" namespace mtconnect::pipeline { + /// @brief Used to manage Agent streaming protocol from upstream agent struct XmlTransformFeedback { uint64_t m_instanceId = 0; @@ -36,10 +37,17 @@ namespace mtconnect::pipeline { }; using namespace mtconnect::entity; + + /// @brief Transform, parse, and map the XML documents extracting the data for feedback class AGENT_LIB_API MTConnectXmlTransform : public Transform { public: MTConnectXmlTransform(const MTConnectXmlTransform &) = default; + + /// @brief Construct a transfor, + /// @param context the pipeline context + /// @param feedback a feedback object to pass back protocol info + /// @param device an associated device MTConnectXmlTransform(PipelineContextPtr context, XmlTransformFeedback &feedback, const std::optional &device = std::nullopt) : Transform("MTConnectXmlTransform"), diff --git a/src/mtconnect/pipeline/period_filter.hpp b/src/mtconnect/pipeline/period_filter.hpp index 0b9acf0e..5ece3ba9 100644 --- a/src/mtconnect/pipeline/period_filter.hpp +++ b/src/mtconnect/pipeline/period_filter.hpp @@ -21,219 +21,224 @@ #include "mtconnect/observation/observation.hpp" #include "transform.hpp" -namespace mtconnect { - class Agent; - namespace pipeline { - class AGENT_LIB_API PeriodFilter : public Transform +namespace mtconnect::pipeline { + /// @brief Period Filter implementing MTConnect DataItem period-filter behavior + class AGENT_LIB_API PeriodFilter : public Transform + { + public: + /// @brief Helper class to save information about the last observation for the period filter + struct LastObservation { - public: - struct LastObservation - { - LastObservation(std::chrono::milliseconds p, boost::asio::io_context::strand &st) - : m_timer(st.context()), m_period(p) - {} + /// @brief Construct a Last Observation + /// @param p the amount of time in the period + /// @param st the strand to use for the time + LastObservation(std::chrono::milliseconds p, boost::asio::io_context::strand &st) + : m_timer(st.context()), m_period(p) + {} - // Make sure the timer is canceled. - ~LastObservation() { m_timer.cancel(); } + /// @brief Make sure the timer is canceled. + ~LastObservation() { m_timer.cancel(); } - // The timestamp o the last observation or timestamp of the adjusted timestamp to - // the end of the last scheduled send time. - Timestamp m_timestamp; + /// @brief The timestamp o the last observation or timestamp of the adjusted timestamp to + /// the end of the last scheduled send time. + Timestamp m_timestamp; - // The delayed observation. - observation::ObservationPtr m_observation; + /// @brief The delayed observation. + observation::ObservationPtr m_observation; - // A timer for delayed sends. - boost::asio::steady_timer m_timer; + /// @brief A timer for delayed sends. + boost::asio::steady_timer m_timer; - // Store the data item period here. - std::chrono::milliseconds m_period; + /// @brief Store the data item period here. + std::chrono::milliseconds m_period; - // Time from the current obervation to the end of the period. - std::chrono::milliseconds m_delta; - }; + /// @brief Time from the current obervation to the end of the period. + std::chrono::milliseconds m_delta; + }; - using LastObservationMap = std::unordered_map; - using LastObservationIterator = LastObservationMap::iterator; + using LastObservationMap = std::unordered_map; + using LastObservationIterator = LastObservationMap::iterator; - struct State : TransformState - { - LastObservationMap m_lastObservation; + /// @brief A shared state variable containing the last observation + struct State : TransformState + { + LastObservationMap m_lastObservation; + }; + + /// @brief Construct a period filter with a context + /// @param context the context + /// @param st strand for the timer + PeriodFilter(PipelineContextPtr context, boost::asio::io_context::strand &st) + : Transform("PeriodFilter"), + m_state(context->getSharedState(m_name)), + m_contract(context->m_contract.get()), + m_strand(st) + { + using namespace observation; + constexpr static auto lambda = [](const Observation &s) { + return bool(!s.isOrphan() && s.getDataItem()->getMinimumPeriod()); }; + m_guard = LambdaGuard>(lambda, RUN) || + TypeGuard(SKIP); + } + ~PeriodFilter() override = default; - PeriodFilter(PipelineContextPtr context, boost::asio::io_context::strand &st) - : Transform("PeriodFilter"), - m_state(context->getSharedState(m_name)), - m_contract(context->m_contract.get()), - m_strand(st) - { - using namespace observation; - constexpr static auto lambda = [](const Observation &s) { - return bool(!s.isOrphan() && s.getDataItem()->getMinimumPeriod()); - }; - m_guard = LambdaGuard>(lambda, RUN) || - TypeGuard(SKIP); - } - ~PeriodFilter() override = default; + const entity::EntityPtr operator()(const entity::EntityPtr entity) override + { + using namespace std; + using namespace observation; + using namespace entity; - const entity::EntityPtr operator()(const entity::EntityPtr entity) override + auto obs = std::dynamic_pointer_cast(entity); { - using namespace std; - using namespace observation; - using namespace entity; + std::lock_guard guard(*m_state); - auto obs = std::dynamic_pointer_cast(entity); - { - std::lock_guard guard(*m_state); + if (obs->isOrphan()) + return EntityPtr(); - if (obs->isOrphan()) - return EntityPtr(); + auto di = obs->getDataItem(); + auto &id = di->getId(); - auto di = obs->getDataItem(); - auto &id = di->getId(); + if (obs->isUnavailable()) + { + m_state->m_lastObservation.erase(id); + } + else + { + auto ts = obs->getTimestamp(); - if (obs->isUnavailable()) - { - m_state->m_lastObservation.erase(id); - } - else + auto last = m_state->m_lastObservation.find(id); + if (last == m_state->m_lastObservation.end()) { - auto ts = obs->getTimestamp(); - - auto last = m_state->m_lastObservation.find(id); - if (last == m_state->m_lastObservation.end()) + auto period = + chrono::milliseconds(static_cast(*di->getMinimumPeriod() * 1000.0)); + auto res = m_state->m_lastObservation.try_emplace(id, period, m_strand); + if (res.second) + last = res.first; + else { - auto period = - chrono::milliseconds(static_cast(*di->getMinimumPeriod() * 1000.0)); - auto res = m_state->m_lastObservation.try_emplace(id, period, m_strand); - if (res.second) - last = res.first; - else - { - LOG(error) << "PeriodFilter cannot create last observation"; - return EntityPtr(); - } - } - - // If filtered, return an empty entity. - if (filtered(last->second, id, obs, ts)) + LOG(error) << "PeriodFilter cannot create last observation"; return EntityPtr(); + } } - } - return next(obs); + // If filtered, return an empty entity. + if (filtered(last->second, id, obs, ts)) + return EntityPtr(); + } } - protected: - // Returns true if the observation is filtered. - bool filtered(LastObservation &last, const std::string &id, observation::ObservationPtr &obs, - const Timestamp &ts) + return next(obs); + } + + protected: + // Returns true if the observation is filtered. + bool filtered(LastObservation &last, const std::string &id, observation::ObservationPtr &obs, + const Timestamp &ts) + { + using namespace std; + using namespace chrono; + using namespace observation; + + auto delta = duration_cast(ts - last.m_timestamp); + if (delta.count() >= 0 && delta < last.m_period) { - using namespace std; - using namespace chrono; - using namespace observation; + bool observed = bool(last.m_observation); + last.m_observation = obs; + last.m_delta = last.m_period - delta; + + // If we have not already observed something for this period, + // set a timer, otherwise the current observation will replace the last + // and be triggered when the timer expires. The end of the period is still the + // same, so keep the timer as is. + if (!observed) + delayDelivery(last, id); - auto delta = duration_cast(ts - last.m_timestamp); - if (delta.count() >= 0 && delta < last.m_period) - { - bool observed = bool(last.m_observation); - last.m_observation = obs; - last.m_delta = last.m_period - delta; - - // If we have not already observed something for this period, - // set a timer, otherwise the current observation will replace the last - // and be triggered when the timer expires. The end of the period is still the - // same, so keep the timer as is. - if (!observed) - delayDelivery(last, id); - - // Filter this observation. - return true; - } - else if (last.m_observation && delta >= last.m_period && delta < last.m_period * 2) - { - last.m_observation.swap(obs); + // Filter this observation. + return true; + } + else if (last.m_observation && delta >= last.m_period && delta < last.m_period * 2) + { + last.m_observation.swap(obs); - // Similar to the delayed send, the last timestamp is computed as the end - // of the previous period. - last.m_timestamp = obs->getTimestamp() + last.m_delta; + // Similar to the delayed send, the last timestamp is computed as the end + // of the previous period. + last.m_timestamp = obs->getTimestamp() + last.m_delta; - // Compute the distance to the next period and delay delivery of this observation. - last.m_delta = last.m_period * 2 - delta; + // Compute the distance to the next period and delay delivery of this observation. + last.m_delta = last.m_period * 2 - delta; - delayDelivery(last, id); + delayDelivery(last, id); - // The observations will be swapped, so send the last onward. - return false; - } - else + // The observations will be swapped, so send the last onward. + return false; + } + else + { + // If this observation is after the period has expired and there + // is an existing obsrvation, then we send the last observation. + if (last.m_observation) { - // If this observation is after the period has expired and there - // is an existing obsrvation, then we send the last observation. - if (last.m_observation) - { - last.m_timer.cancel(); - next(last.m_observation); - last.m_observation.reset(); - } + last.m_timer.cancel(); + next(last.m_observation); + last.m_observation.reset(); + } - // Set the timestamp of the last observation. - last.m_timestamp = ts; + // Set the timestamp of the last observation. + last.m_timestamp = ts; - // Send this observation. This may send two observations. - return false; - } + // Send this observation. This may send two observations. + return false; } + } - void delayDelivery(LastObservation &last, const std::string &id) - { - using boost::placeholders::_1; - - // Set the timer to expire in the remaining time left in the period given - // in last.m_delta - last.m_timer.cancel(); - last.m_timer.expires_after(last.m_delta); - - // Bind the strand so we do not have races. Use the data item id so there are - // no race conditions due to LastObservation lifecycle. - last.m_timer.async_wait([this, id](boost::system::error_code ec) { - boost::asio::dispatch(m_strand, - boost::bind(&PeriodFilter::sendObservation, this, id, ec)); - }); - } + void delayDelivery(LastObservation &last, const std::string &id) + { + using boost::placeholders::_1; + + // Set the timer to expire in the remaining time left in the period given + // in last.m_delta + last.m_timer.cancel(); + last.m_timer.expires_after(last.m_delta); + + // Bind the strand so we do not have races. Use the data item id so there are + // no race conditions due to LastObservation lifecycle. + last.m_timer.async_wait([this, id](boost::system::error_code ec) { + boost::asio::dispatch(m_strand, boost::bind(&PeriodFilter::sendObservation, this, id, ec)); + }); + } - void sendObservation(const std::string id, boost::system::error_code ec) + void sendObservation(const std::string id, boost::system::error_code ec) + { + if (!ec) { - if (!ec) + using namespace std; + using namespace observation; + + ObservationPtr obs; { - using namespace std; - using namespace observation; + std::lock_guard guard(*m_state); - ObservationPtr obs; + // Find the entry for this data item and make sure there is an observation + auto last = m_state->m_lastObservation.find(id); + if (last != m_state->m_lastObservation.end() && last->second.m_observation) { - std::lock_guard guard(*m_state); - - // Find the entry for this data item and make sure there is an observation - auto last = m_state->m_lastObservation.find(id); - if (last != m_state->m_lastObservation.end() && last->second.m_observation) - { - last->second.m_observation.swap(obs); - last->second.m_timestamp = obs->getTimestamp() + last->second.m_delta; - } + last->second.m_observation.swap(obs); + last->second.m_timestamp = obs->getTimestamp() + last->second.m_delta; } + } - // Send the observation onward - if (obs) - { - next(obs); - } + // Send the observation onward + if (obs) + { + next(obs); } } - - protected: - std::shared_ptr m_state; - PipelineContract *m_contract; - boost::asio::io_context::strand &m_strand; - }; - } // namespace pipeline -} // namespace mtconnect + } + + protected: + std::shared_ptr m_state; + PipelineContract *m_contract; + boost::asio::io_context::strand &m_strand; + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/pipeline.hpp b/src/mtconnect/pipeline/pipeline.hpp index 64b8fd39..e746091b 100644 --- a/src/mtconnect/pipeline/pipeline.hpp +++ b/src/mtconnect/pipeline/pipeline.hpp @@ -37,20 +37,42 @@ namespace mtconnect { namespace source::adapter { class Adapter; } // namespace source::adapter + + /// @brief mtconnect pipelines and transformations + /// + /// Contains all classes pertaining to pipeline transformations namespace pipeline { + /// @brief Abstract Pipeline class + /// + /// Must be subclassed and the `build()` method must be provided class AGENT_LIB_API Pipeline { public: + /// @brief A splice function type for resplicing the pipeline after it is rebuilt using Splice = std::function; + /// @brief Pipeline constructor + /// @param context The pipeline context + /// @param st boost asio strand for for setting timers and running async operations + /// @note All pipelines run in a single strand (thread) and therefor all operations are + /// thread-safe in one pipeline. + Pipeline(PipelineContextPtr context, boost::asio::io_context::strand &st) : m_start(std::make_shared()), m_context(context), m_strand(st) {} + /// @brief Destructor stops the pipeline virtual ~Pipeline() { m_start->stop(); } + /// @brief Build the pipeline + /// @param options A set of configuration options virtual void build(const ConfigOptions &options) = 0; + /// @brief Has the pipeline started? + /// @return `true` if started bool started() const { return m_started; } + /// @brief Get a reference to the strand + /// @return the strand boost::asio::io_context::strand &getStrand() { return m_strand; } + /// @brief Apply the splices after rebuilding void applySplices() { for (auto &splice : m_splices) diff --git a/src/mtconnect/pipeline/pipeline_context.hpp b/src/mtconnect/pipeline/pipeline_context.hpp index 63b1fdcd..89d3b58f 100644 --- a/src/mtconnect/pipeline/pipeline_context.hpp +++ b/src/mtconnect/pipeline/pipeline_context.hpp @@ -24,40 +24,54 @@ #include "mtconnect/config.hpp" #include "pipeline_contract.hpp" -namespace mtconnect { - namespace pipeline { - struct TransformState - { - // For mutex locking - auto lock() { return m_mutex.lock(); } - auto unlock() { return m_mutex.unlock(); } - auto try_lock() { return m_mutex.try_lock(); } +namespace mtconnect::pipeline { + /// @brief Base class for all shared state used by the `PipelineContext` + struct TransformState + { + /// @brief Forwards to `std::mutex` `lock()` + /// @return void + auto lock() { return m_mutex.lock(); } + /// @brief Forwards to `std::mutex` `unlock()` + /// @return void + auto unlock() { return m_mutex.unlock(); } + /// @brief Forwards to `std::mutex` `try_lock()` + /// @return `true` if sucessful + auto try_lock() { return m_mutex.try_lock(); } + + /// @brief The mutex to lock for synchronized access + std::mutex m_mutex; + virtual ~TransformState() {} + }; + using TransformStatePtr = std::shared_ptr; - std::mutex m_mutex; - virtual ~TransformState() {} - }; - using TransformStatePtr = std::shared_ptr; + /// @brief Manages shared state across multiple pipelines + /// + /// Used for cases like duplicate detection and shared counters. + class AGENT_LIB_API PipelineContext : public std::enable_shared_from_this + { + public: + auto getptr() { return shared_from_this(); } - class AGENT_LIB_API PipelineContext : public std::enable_shared_from_this + /// @brief Retrieves the shared state for a given name. + /// @tparam T the type of the shared state. Must be a subclass of TransformState or a lockable + /// object. + /// @param[in] name the name of the shared state + /// @return a shared pointer to the shared state. + template + std::shared_ptr getSharedState(const std::string &name) { - public: - auto getptr() { return shared_from_this(); } - - template - std::shared_ptr getSharedState(const std::string &name) - { - auto &state = m_sharedState[name]; - if (!state) - state = std::make_shared(); - return std::dynamic_pointer_cast(state); - } - - std::unique_ptr m_contract; - - protected: - using SharedState = std::unordered_map; - SharedState m_sharedState; - }; - using PipelineContextPtr = std::shared_ptr; - } // namespace pipeline -} // namespace mtconnect + auto &state = m_sharedState[name]; + if (!state) + state = std::make_shared(); + return std::dynamic_pointer_cast(state); + } + + /// @brief A pipeline contract that can be used by the shared state. + std::unique_ptr m_contract; + + protected: + using SharedState = std::unordered_map; + SharedState m_sharedState; + }; + using PipelineContextPtr = std::shared_ptr; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/pipeline_contract.hpp b/src/mtconnect/pipeline/pipeline_contract.hpp index 51f82448..77976f63 100644 --- a/src/mtconnect/pipeline/pipeline_contract.hpp +++ b/src/mtconnect/pipeline/pipeline_contract.hpp @@ -48,6 +48,10 @@ namespace mtconnect { } // namespace entity namespace pipeline { + /// @brief The interface required by a pipeline to deliver and get information + /// + /// Provides the necessary methods for the pipeline to deliver entities and + /// retrieve information about devices class AGENT_LIB_API PipelineContract { public: @@ -56,16 +60,41 @@ namespace mtconnect { using EachDataItem = std::function; + /// @brief Find a device by name or uuid + /// @param[in] device device name or uuid + /// @return shared pointer to the device if found virtual DevicePtr findDevice(const std::string &device) = 0; + /// @brief Find a data item for a device by name. + /// @param[in] device name or uuid of the device + /// @param[in] name name or id of the data item + /// @return shared pointer to the data item if found virtual DataItemPtr findDataItem(const std::string &device, const std::string &name) = 0; + /// @brief iterate through all the data items calling `fun` for each + /// @param[in] fun The function or lambda to call virtual void eachDataItem(EachDataItem fun) = 0; - virtual void deliverObservation(observation::ObservationPtr) = 0; - virtual void deliverAsset(asset::AssetPtr) = 0; + /// @brief deliver an observation to the circular buffer and the sinks + /// @param[in] obs a shared pointer to the observation + virtual void deliverObservation(observation::ObservationPtr obs) = 0; + /// @brief deliver an asset to the asset storage + /// @param[in] asset the asset to deliver + virtual void deliverAsset(asset::AssetPtr asset) = 0; + /// @brief Deliver a device to the agent. + /// @param[in] device the new or changed device virtual void deliverDevice(DevicePtr device) = 0; - virtual void deliverAssetCommand(entity::EntityPtr) = 0; - virtual void deliverCommand(entity::EntityPtr) = 0; - virtual void deliverConnectStatus(entity::EntityPtr, const StringList &devices, + /// @brief Deliver a command, remove or remova all + /// @param[in] command the command + virtual void deliverAssetCommand(entity::EntityPtr command) = 0; + /// @brief Deliver an agent related command + /// @param[in] command the command + virtual void deliverCommand(entity::EntityPtr command) = 0; + /// @brief Notify receiver of the status of a data source + /// @param[in] status the status of the source + /// @param[in] devices a list of known devices + /// @param[in] autoAvailable if the connection status should change availability + virtual void deliverConnectStatus(entity::EntityPtr status, const StringList &devices, bool autoAvailable) = 0; + /// @brief The source is no longer viable, do not try to reconnect + /// @param[in] identity the identity of the source virtual void sourceFailed(const std::string &identity) = 0; }; } // namespace pipeline diff --git a/src/mtconnect/pipeline/response_document.hpp b/src/mtconnect/pipeline/response_document.hpp index 50471ffb..a2e2421e 100644 --- a/src/mtconnect/pipeline/response_document.hpp +++ b/src/mtconnect/pipeline/response_document.hpp @@ -26,8 +26,10 @@ namespace mtconnect::pipeline { using namespace mtconnect; + /// @brief Utility class for parsing XML response document struct AGENT_LIB_API ResponseDocument { + /// @brief An error document response from the agent struct Error { std::string m_code; @@ -35,16 +37,21 @@ namespace mtconnect::pipeline { }; using Errors = std::list; + /// @brief parse the content of the XML document + /// @param[in] content XML document + /// @param[out] doc the created response document + /// @param[in] context pipeline context + /// @param[in] device optional device uuid + /// @return `true` if successful static bool parse(const std::string_view &content, ResponseDocument &doc, pipeline::PipelineContextPtr context, const std::optional &device = std::nullopt); // Parsed data - SequenceNumber_t m_next; - uint64_t m_instanceId; - entity::EntityList m_entities; - entity::EntityList m_assetEvents; - Errors m_errors; + SequenceNumber_t m_next; ///< Next sequence number + uint64_t m_instanceId; ///< Agent instance id + entity::EntityList m_entities; ///< List of entities + entity::EntityList m_assetEvents; ///< List of asset events + Errors m_errors; ///< List of Errors }; - } // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/shdr_token_mapper.hpp b/src/mtconnect/pipeline/shdr_token_mapper.hpp index 579c58a2..8294282d 100644 --- a/src/mtconnect/pipeline/shdr_token_mapper.hpp +++ b/src/mtconnect/pipeline/shdr_token_mapper.hpp @@ -26,48 +26,58 @@ #include "timestamp_extractor.hpp" #include "transform.hpp" -namespace mtconnect { - class Device; +namespace mtconnect::pipeline { + using namespace entity; + /// @brief A list of observations for testing + class AGENT_LIB_API Observations : public Timestamped + { + public: + using Timestamped::Timestamped; + }; - namespace pipeline { - using namespace entity; - class AGENT_LIB_API Observations : public Timestamped + /// @brief Map a token list to data items or asset types + class AGENT_LIB_API ShdrTokenMapper : public Transform + { + public: + ShdrTokenMapper(const ShdrTokenMapper &) = default; + ShdrTokenMapper(PipelineContextPtr context, + const std::optional &device = std::nullopt, int version = 1) + : Transform("ShdrTokenMapper"), + m_contract(context->m_contract.get()), + m_defaultDevice(device), + m_shdrVersion(version) { - public: - using Timestamped::Timestamped; - }; + m_guard = TypeGuard(RUN); + } + const EntityPtr operator()(const EntityPtr entity) override; - class AGENT_LIB_API ShdrTokenMapper : public Transform - { - public: - ShdrTokenMapper(const ShdrTokenMapper &) = default; - ShdrTokenMapper(PipelineContextPtr context, - const std::optional &device = std::nullopt, int version = 1) - : Transform("ShdrTokenMapper"), - m_contract(context->m_contract.get()), - m_defaultDevice(device), - m_shdrVersion(version) - { - m_guard = TypeGuard(RUN); - } - const EntityPtr operator()(const EntityPtr entity) override; - - // Takes a tokenized set of fields and maps them to timestamp and data items - EntityPtr mapTokensToDataItem(const Timestamp ×tamp, - const std::optional &source, - TokenList::const_iterator &token, - const TokenList::const_iterator &end, ErrorList &errors); - EntityPtr mapTokensToAsset(const Timestamp ×tamp, - const std::optional &source, - TokenList::const_iterator &token, - const TokenList::const_iterator &end, ErrorList &errors); + /// @brief Takes a tokenized set of fields and maps them data items + /// @param[in] timestamp the timestamp from prior extraction + /// @param[in] source the optional source + /// @param[in] token a token itertor + /// @param[in] end the sentinal end token + /// @param[in,out] errors + /// @return returns an observation list + EntityPtr mapTokensToDataItem(const Timestamp ×tamp, + const std::optional &source, + TokenList::const_iterator &token, + const TokenList::const_iterator &end, ErrorList &errors); + /// @brief Takes a tokenized set of fields and maps them to assets + /// @param timestamp the timestamp + /// @param source the optional source + /// @param[in] token a token itertor + /// @param[in] end the sentinal end token + /// @param[in,out] errors + /// @return An asset + EntityPtr mapTokensToAsset(const Timestamp ×tamp, const std::optional &source, + TokenList::const_iterator &token, + const TokenList::const_iterator &end, ErrorList &errors); - protected: - // Logging Context - std::set m_logOnce; - PipelineContract *m_contract; - std::optional m_defaultDevice; - int m_shdrVersion {1}; - }; - } // namespace pipeline -} // namespace mtconnect + protected: + // Logging Context + std::set m_logOnce; + PipelineContract *m_contract; + std::optional m_defaultDevice; + int m_shdrVersion {1}; + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/shdr_tokenizer.hpp b/src/mtconnect/pipeline/shdr_tokenizer.hpp index 7e09090e..b9f48f1d 100644 --- a/src/mtconnect/pipeline/shdr_tokenizer.hpp +++ b/src/mtconnect/pipeline/shdr_tokenizer.hpp @@ -24,146 +24,145 @@ #include "mtconnect/entity/entity.hpp" #include "transform.hpp" -namespace mtconnect { - class Agent; - - namespace pipeline { - using TokenList = std::list; - class AGENT_LIB_API Tokens : public entity::Entity +namespace mtconnect::pipeline { + /// @brief A list of strings + using TokenList = std::list; + /// @brief An entity that has carries list of tokens + class AGENT_LIB_API Tokens : public entity::Entity + { + public: + using entity::Entity::Entity; + Tokens(const Tokens &) = default; + Tokens() = default; + Tokens(const Tokens &ts, TokenList list) : Entity(ts), m_tokens(list) {} + + TokenList m_tokens; + }; + + /// @brief Splits a line of SHDR into fields using a pipe (`|`) delimeter + class AGENT_LIB_API ShdrTokenizer : public Transform + { + public: + ShdrTokenizer(const ShdrTokenizer &) = default; + ShdrTokenizer() : Transform("ShdrTokenizer") { m_guard = EntityNameGuard("Data", RUN); } + ~ShdrTokenizer() = default; + + const entity::EntityPtr operator()(const entity::EntityPtr data) override + { + auto &body = data->getValue(); + entity::Properties props; + if (auto source = data->maybeGet("source")) + props["source"] = *source; + auto result = std::make_shared("Tokens", props); + tokenize(body, result->m_tokens); + return next(result); + } + + template + inline static std::string remove(const T &range, const char c) { - public: - using entity::Entity::Entity; - Tokens(const Tokens &) = default; - Tokens() = default; - Tokens(const Tokens &ts, TokenList list) : Entity(ts), m_tokens(list) {} + using namespace std; + string res; + copy_if(range.first, range.second, std::back_inserter(res), + [&c](const char m) { return c != m; }); - TokenList m_tokens; - }; + return res; + } - class AGENT_LIB_API ShdrTokenizer : public Transform + inline static std::string trim(const std::string &str) { - public: - ShdrTokenizer(const ShdrTokenizer &) = default; - ShdrTokenizer() : Transform("ShdrTokenizer") { m_guard = EntityNameGuard("Data", RUN); } - ~ShdrTokenizer() = default; + using namespace std; - const entity::EntityPtr operator()(const entity::EntityPtr data) override - { - auto &body = data->getValue(); - entity::Properties props; - if (auto source = data->maybeGet("source")) - props["source"] = *source; - auto result = std::make_shared("Tokens", props); - tokenize(body, result->m_tokens); - return next(result); - } + auto first = str.find_first_not_of(" \r\n\t"); + auto last = str.find_last_not_of(" \r\n\t"); - template - inline static std::string remove(const T &range, const char c) - { - using namespace std; - string res; - copy_if(range.first, range.second, std::back_inserter(res), - [&c](const char m) { return c != m; }); + if (first == string::npos) + return ""; + else if (first == 0 && last == str.length() - 1) + return str; + else + return str.substr(first, last - first + 1); + } - return res; - } - - inline static std::string trim(const std::string &str) + static inline void tokenize(const std::string &data, TokenList &tokens) + { + using namespace std; + auto cp = data.c_str(); + std::string token; + bool copied {false}; + while (*cp != '\0') { - using namespace std; - - auto first = str.find_first_not_of(" \r\n\t"); - auto last = str.find_last_not_of(" \r\n\t"); + while (*cp != '\0' && isspace(*cp)) + cp++; - if (first == string::npos) - return ""; - else if (first == 0 && last == str.length() - 1) - return str; - else - return str.substr(first, last - first + 1); - } - - static inline void tokenize(const std::string &data, TokenList &tokens) - { - using namespace std; - auto cp = data.c_str(); - std::string token; - bool copied {false}; - while (*cp != '\0') + auto start = cp, orig = cp; + const char *end = 0; + if (*cp == '"') { - while (*cp != '\0' && isspace(*cp)) - cp++; - - auto start = cp, orig = cp; - const char *end = 0; - if (*cp == '"') + cp = ++start; + while (*cp != '\0') { - cp = ++start; - while (*cp != '\0') + if (*cp == '\\') { - if (*cp == '\\') + if (!copied) { - if (!copied) - { - token = start; - size_t dist = cp - start; - start = token.c_str(); - cp = start + dist; - copied = true; - } - memmove(const_cast(cp), cp + 1, strlen(cp)); + token = start; + size_t dist = cp - start; + start = token.c_str(); + cp = start + dist; + copied = true; } - else if (*cp == '|') - { - break; - } - else if (*cp == '"') - { - // Make sure there is a | or the string ends after the - // terminal ". Skip spaces. - auto nc = cp + 1; - while (*nc != '\0' && isspace(*nc)) - nc++; - if (*nc == '|' || *nc == '\0') - end = cp; - else - break; - } - - if (*cp != '\0') - cp++; + memmove(const_cast(cp), cp + 1, strlen(cp)); } - // If there was no terminating '"' - if (end == 0 && copied) + else if (*cp == '|') { - // Undo copy - cp = start = orig; - while (*cp != '|' && *cp != '\0') - cp++; + break; } + else if (*cp == '"') + { + // Make sure there is a | or the string ends after the + // terminal ". Skip spaces. + auto nc = cp + 1; + while (*nc != '\0' && isspace(*nc)) + nc++; + if (*nc == '|' || *nc == '\0') + end = cp; + else + break; + } + + if (*cp != '\0') + cp++; } - else + // If there was no terminating '"' + if (end == 0 && copied) { + // Undo copy + cp = start = orig; while (*cp != '|' && *cp != '\0') cp++; } + } + else + { + while (*cp != '|' && *cp != '\0') + cp++; + } - if (end == 0) - end = cp; + if (end == 0) + end = cp; - while (end > start && isspace(*(end - 1))) - end--; + while (end > start && isspace(*(end - 1))) + end--; - tokens.emplace_back(start, end); + tokens.emplace_back(start, end); - // Handle terminal '|' - if (*cp == '|' && *(cp + 1) == '\0') - tokens.emplace_back(""); - if (*cp != '\0') - cp++; - } + // Handle terminal '|' + if (*cp == '|' && *(cp + 1) == '\0') + tokens.emplace_back(""); + if (*cp != '\0') + cp++; } - }; - } // namespace pipeline -} // namespace mtconnect + } + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/timestamp_extractor.hpp b/src/mtconnect/pipeline/timestamp_extractor.hpp index 406b5900..3ea04855 100644 --- a/src/mtconnect/pipeline/timestamp_extractor.hpp +++ b/src/mtconnect/pipeline/timestamp_extractor.hpp @@ -22,109 +22,111 @@ #include "shdr_tokenizer.hpp" #include "transform.hpp" -namespace mtconnect { - class Agent; - - namespace pipeline { - using namespace entity; - - class AGENT_LIB_API Timestamped : public Tokens +namespace mtconnect::pipeline { + using namespace entity; + + /// @brief An entity that carries a timestamp and a token list + class AGENT_LIB_API Timestamped : public Tokens + { + public: + using Tokens::Tokens; + Timestamped(const Timestamped &ts) = default; + Timestamped(const Tokens &ptr) : Tokens(ptr) {} + Timestamped(const Timestamped &ts, TokenList list) + : Tokens(ts, list), m_timestamp(ts.m_timestamp), m_duration(ts.m_duration) + {} + ~Timestamped() = default; + Timestamp m_timestamp; + std::optional m_duration; ///< Optional duration + }; + using TimestampedPtr = std::shared_ptr; + + /// @brief A timestamped asset command + class AGENT_LIB_API AssetCommand : public Timestamped + { + public: + using Timestamped::Timestamped; + }; + + /// @brief A transform to extract the timestamp + class AGENT_LIB_API ExtractTimestamp : public Transform + { + public: + ExtractTimestamp(const ExtractTimestamp &) = default; + /// @brief Construct a timestamp extractor + /// @param relativeTime `true` if using realtive time stamps + ExtractTimestamp(bool relativeTime) + : Transform("ExtractTimestamp"), m_relativeTime(relativeTime) { - public: - using Tokens::Tokens; - Timestamped(const Timestamped &ts) = default; - Timestamped(const Tokens &ptr) : Tokens(ptr) {} - Timestamped(const Timestamped &ts, TokenList list) - : Tokens(ts, list), m_timestamp(ts.m_timestamp), m_duration(ts.m_duration) - {} - ~Timestamped() = default; - Timestamp m_timestamp; - std::optional m_duration; - }; - using TimestampedPtr = std::shared_ptr; + m_guard = TypeGuard(RUN); + } + ~ExtractTimestamp() override = default; - class AGENT_LIB_API AssetCommand : public Timestamped + using Now = std::function; + const EntityPtr operator()(const EntityPtr ptr) override { - public: - using Timestamped::Timestamped; - }; - - class AGENT_LIB_API ExtractTimestamp : public Transform - { - public: - ExtractTimestamp(const ExtractTimestamp &) = default; - ExtractTimestamp(bool relativeTime) - : Transform("ExtractTimestamp"), m_relativeTime(relativeTime) + TimestampedPtr res; + std::optional token; + if (auto tokens = std::dynamic_pointer_cast(ptr); + tokens && tokens->m_tokens.size() > 0) { - m_guard = TypeGuard(RUN); + res = std::make_shared(*tokens); + token = res->m_tokens.front(); + res->m_tokens.pop_front(); } - ~ExtractTimestamp() override = default; - - using Now = std::function; - const EntityPtr operator()(const EntityPtr ptr) override + else if (ptr->hasProperty("timestamp")) { - TimestampedPtr res; - std::optional token; - if (auto tokens = std::dynamic_pointer_cast(ptr); - tokens && tokens->m_tokens.size() > 0) - { - res = std::make_shared(*tokens); - token = res->m_tokens.front(); - res->m_tokens.pop_front(); - } - else if (ptr->hasProperty("timestamp")) - { - token = res->maybeGet("timestamp"); - if (token) - res->erase("timestamp"); - } - + token = res->maybeGet("timestamp"); if (token) - extractTimestamp(*token, res); - else - res->m_timestamp = now(); - - res->setProperty("timestamp", res->m_timestamp); - return next(res); + res->erase("timestamp"); } - void extractTimestamp(const std::string &token, TimestampedPtr &ts); - inline Timestamp now() { return m_now ? m_now() : std::chrono::system_clock::now(); } + if (token) + extractTimestamp(*token, res); + else + res->m_timestamp = now(); - Now m_now; + res->setProperty("timestamp", res->m_timestamp); + return next(res); + } - protected: - bool m_relativeTime {false}; - std::optional m_base; - Microseconds m_offset; - }; + void extractTimestamp(const std::string &token, TimestampedPtr &ts); + inline Timestamp now() { return m_now ? m_now() : std::chrono::system_clock::now(); } - class AGENT_LIB_API IgnoreTimestamp : public ExtractTimestamp - { - public: - IgnoreTimestamp() : ExtractTimestamp("IgnoreTimestamp") {} - IgnoreTimestamp(const IgnoreTimestamp &) = default; - ~IgnoreTimestamp() override = default; + Now m_now; - const EntityPtr operator()(const EntityPtr ptr) override - { - TimestampedPtr res; - std::optional token; - if (auto tokens = std::dynamic_pointer_cast(ptr); - tokens && tokens->m_tokens.size() > 0) - { - res = std::make_shared(*tokens); - res->m_tokens.pop_front(); - } - else if (res->hasProperty("timestamp")) - { - res->erase("timestamp"); - } - res->m_timestamp = now(); - res->setProperty("timestamp", res->m_timestamp); + protected: + bool m_relativeTime {false}; + std::optional m_base; + Microseconds m_offset; + }; - return next(res); + /// @brief Always use agent time and remove first token + class AGENT_LIB_API IgnoreTimestamp : public ExtractTimestamp + { + public: + IgnoreTimestamp() : ExtractTimestamp("IgnoreTimestamp") {} + IgnoreTimestamp(const IgnoreTimestamp &) = default; + ~IgnoreTimestamp() override = default; + + const EntityPtr operator()(const EntityPtr ptr) override + { + TimestampedPtr res; + std::optional token; + if (auto tokens = std::dynamic_pointer_cast(ptr); + tokens && tokens->m_tokens.size() > 0) + { + res = std::make_shared(*tokens); + res->m_tokens.pop_front(); + } + else if (res->hasProperty("timestamp")) + { + res->erase("timestamp"); } - }; - } // namespace pipeline -} // namespace mtconnect + res->m_timestamp = now(); + res->setProperty("timestamp", res->m_timestamp); + + return next(res); + } + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/topic_mapper.hpp b/src/mtconnect/pipeline/topic_mapper.hpp index a0d5c70e..8fe9e922 100644 --- a/src/mtconnect/pipeline/topic_mapper.hpp +++ b/src/mtconnect/pipeline/topic_mapper.hpp @@ -30,144 +30,156 @@ #include "timestamp_extractor.hpp" #include "transform.hpp" -namespace mtconnect { - namespace pipeline { - class AGENT_LIB_API PipelineMessage : public Entity +namespace mtconnect::pipeline { + /// @brief A message from a pub/sub messaging protocol + class AGENT_LIB_API PipelineMessage : public Entity + { + public: + using Entity::Entity; + ~PipelineMessage() = default; + + DataItemPtr m_dataItem; ///< Mapped data item + std::weak_ptr m_device; ///< Mapped device + }; + using PipelineMessagePtr = std::shared_ptr; + + /// @brief A JsonMessage un-parsed + class AGENT_LIB_API JsonMessage : public PipelineMessage + { + public: + using PipelineMessage::PipelineMessage; + }; + + /// @brief An unparsed data message + class AGENT_LIB_API DataMessage : public PipelineMessage + { + public: + using PipelineMessage::PipelineMessage; + }; + + /// @brief A transform to map the topic to a data item + class AGENT_LIB_API TopicMapper : public Transform + { + public: + TopicMapper(const TopicMapper &) = default; + TopicMapper(PipelineContextPtr context, const std::optional &device = std::nullopt) + : Transform("TopicMapper"), m_context(context), m_defaultDevice(device) { - public: - using Entity::Entity; - ~PipelineMessage() = default; + m_guard = EntityNameGuard("Message", RUN); + } + + /// @brief Try to find a matching data item for the given topic + /// + /// Map using the following methods: + /// 1. Try `/` + /// 2. Try the default device and the data item name or id + /// 3. Scan the path for any matching device and data item + /// + /// If found, remember the mapping of the topic to the data item + /// @param topic the topic + /// @return + auto resolve(const std::string &topic) + { + using namespace std; + namespace algo = boost::algorithm; + using namespace boost; - DataItemPtr m_dataItem; - std::weak_ptr m_device; - }; - using PipelineMessagePtr = std::shared_ptr; + DataItemPtr dataItem; + DevicePtr device; - class AGENT_LIB_API JsonMessage : public PipelineMessage - { - public: - using PipelineMessage::PipelineMessage; - }; - class AGENT_LIB_API DataMessage : public PipelineMessage - { - public: - using PipelineMessage::PipelineMessage; - }; + string name, deviceName; - class AGENT_LIB_API TopicMapper : public Transform - { - public: - TopicMapper(const TopicMapper &) = default; - TopicMapper(PipelineContextPtr context, - const std::optional &device = std::nullopt) - : Transform("TopicMapper"), m_context(context), m_defaultDevice(device) + std::vector path; + boost::split(path, topic, algo::is_any_of("/")); + if (path.size() > 1) { - m_guard = EntityNameGuard("Message", RUN); + deviceName = path[0]; + name = path[1]; + dataItem = m_context->m_contract->findDataItem(deviceName, name); } - auto resolve(const std::string &topic) + if (!dataItem) { - using namespace std; - namespace algo = boost::algorithm; - using namespace boost; - - DataItemPtr dataItem; - DevicePtr device; - - string name, deviceName; - - std::vector path; - boost::split(path, topic, algo::is_any_of("/")); - if (path.size() > 1) - { - deviceName = path[0]; - name = path[1]; - dataItem = m_context->m_contract->findDataItem(deviceName, name); - } + deviceName = m_defaultDevice.value_or(""); + name = topic; + dataItem = m_context->m_contract->findDataItem(deviceName, name); + } - if (!dataItem) - { - deviceName = m_defaultDevice.value_or(""); - name = topic; - dataItem = m_context->m_contract->findDataItem(deviceName, name); - } + if (!dataItem && path.size() > 1) + { + name = path.back(); + dataItem = m_context->m_contract->findDataItem(deviceName, name); + } - if (!dataItem && path.size() > 1) + if (!dataItem) + { + for (auto &tok : path) { - name = path.back(); - dataItem = m_context->m_contract->findDataItem(deviceName, name); + device = m_context->m_contract->findDevice(tok); + if (device) + break; } - if (!dataItem) + if (device) { for (auto &tok : path) { - device = m_context->m_contract->findDevice(tok); - if (device) + dataItem = device->getDeviceDataItem(tok); + if (dataItem) break; } - - if (device) - { - for (auto &tok : path) - { - dataItem = device->getDeviceDataItem(tok); - if (dataItem) - break; - } - } } + } - // Note even if null so we don't have to try again - m_resolved[topic] = dataItem; - m_devices[topic] = device; + // Note even if null so we don't have to try again + m_resolved[topic] = dataItem; + m_devices[topic] = device; - return std::make_tuple(device, dataItem); - } + return std::make_tuple(device, dataItem); + } - const EntityPtr operator()(const EntityPtr entity) override + const EntityPtr operator()(const EntityPtr entity) override + { + auto &body = entity->getValue(); + DataItemPtr dataItem; + DevicePtr device; + entity::Properties props {entity->getProperties()}; + if (auto topic = entity->maybeGet("topic")) { - auto &body = entity->getValue(); - DataItemPtr dataItem; - DevicePtr device; - entity::Properties props {entity->getProperties()}; - if (auto topic = entity->maybeGet("topic")) + if (auto it = m_devices.find(*topic); it != m_devices.end()) { - if (auto it = m_devices.find(*topic); it != m_devices.end()) - { - device = it->second.lock(); - } - if (auto it = m_resolved.find(*topic); it != m_resolved.end()) - { - dataItem = it->second.lock(); - } - if (!dataItem && !device) - { - std::tie(device, dataItem) = resolve(*topic); - } + device = it->second.lock(); } - - PipelineMessagePtr result; - // Check for JSON Message - if (body[0] == '{') + if (auto it = m_resolved.find(*topic); it != m_resolved.end()) { - result = std::make_shared("JsonMessage", props); + dataItem = it->second.lock(); } - else + if (!dataItem && !device) { - result = std::make_shared("DataMessage", props); + std::tie(device, dataItem) = resolve(*topic); } - result->m_dataItem = dataItem; - result->m_device = device; - - return next(result); } - protected: - PipelineContextPtr m_context; - std::optional m_defaultDevice; - std::unordered_map> m_resolved; - std::unordered_map> m_devices; - }; - } // namespace pipeline -} // namespace mtconnect + PipelineMessagePtr result; + // Check for JSON Message + if (body[0] == '{') + { + result = std::make_shared("JsonMessage", props); + } + else + { + result = std::make_shared("DataMessage", props); + } + result->m_dataItem = dataItem; + result->m_device = device; + + return next(result); + } + + protected: + PipelineContextPtr m_context; + std::optional m_defaultDevice; + std::unordered_map> m_resolved; + std::unordered_map> m_devices; + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/pipeline/transform.hpp b/src/mtconnect/pipeline/transform.hpp index a5c0aba0..7ee889cb 100644 --- a/src/mtconnect/pipeline/transform.hpp +++ b/src/mtconnect/pipeline/transform.hpp @@ -47,27 +47,36 @@ namespace mtconnect { using EachDataItem = std::function; using FindDataItem = std::function; + /// @brief Abstract entity transformation class AGENT_LIB_API Transform : public std::enable_shared_from_this { public: Transform(const Transform &) = default; + /// @brief Construct a transform with a name + /// @param[in] name transform name Transform(const std::string &name) : m_name(name) {} virtual ~Transform() = default; + /// @brief Get the transform name + /// @return the name auto &getName() const { return m_name; } + /// @brief stop this transform and all the following transforms virtual void stop() { for (auto &t : m_next) t->stop(); } + /// @brief start the transform on a strand and all the following transforms + /// @param st the strand virtual void start(boost::asio::io_context::strand &st) { for (auto &t : m_next) t->start(st); } + /// @brief remove all the next transforms virtual void clear() { for (auto &t : m_next) @@ -75,13 +84,22 @@ namespace mtconnect { unlink(); } + /// @brief clear the list of next transforms virtual void unlink() { m_next.clear(); } + /// @brief the transform method must be overloaded + /// @param entity the entity + /// @return the resulting entity virtual const entity::EntityPtr operator()(const entity::EntityPtr entity) = 0; TransformPtr getptr() { return shared_from_this(); } + /// @brief get the list of next transforms + /// @return the list of following transforms TransformList &getNext() { return m_next; } + /// @brief Find the next transform to forward the entity on to + /// @param entity the entity + /// @return return the result of the transformation const entity::EntityPtr next(const entity::EntityPtr entity) { if (m_next.empty()) @@ -111,12 +129,18 @@ namespace mtconnect { return EntityPtr(); } + /// @brief Add the transform to the end of the transform list + /// @param[in] trans the transform + /// @return trans TransformPtr bind(TransformPtr trans) { m_next.emplace_back(trans); return trans; } + /// @brief get the guard action for an entity + /// @param[in] entity the entity + /// @return the action to perform GuardAction check(const entity::EntityPtr entity) { if (!m_guard) @@ -124,12 +148,20 @@ namespace mtconnect { else return m_guard(entity); } + + /// @brief Get a reference to the guard + /// @return the guard const Guard &getGuard() const { return m_guard; } + /// @brief set the guard + /// @param guard a guard void setGuard(const Guard &guard) { m_guard = guard; } using TransformPair = std::pair; using ListOfTransforms = std::list; + /// @brief recursive step to find all transforms with a given name + /// @param[in] target the target transform name + /// @param[out] xforms the list of transform pairs void findRec(const std::string &target, ListOfTransforms &xforms) { for (auto &t : m_next) @@ -142,6 +174,9 @@ namespace mtconnect { } } + /// @brief find all transforms with a given name + /// @param[in] target the target transform name + /// @param[out] xforms the transform pairs void find(const std::string &target, ListOfTransforms &xforms) { if (m_name == target) @@ -152,6 +187,9 @@ namespace mtconnect { findRec(target, xforms); } + /// @brief splice a transform before another transform + /// @param old the transform to splice before + /// @param xform the transform to bind to old void spliceBefore(TransformPtr old, TransformPtr xform) { for (auto it = m_next.begin(); it != m_next.end(); it++) @@ -164,6 +202,11 @@ namespace mtconnect { } } } + + /// @brief Splice after all occurrences of this transform + /// + /// binds xform to all the next transforms and then binds this transform to xform. + /// @param xform the transform to splice after void spliceAfter(TransformPtr xform) { for (auto it = m_next.begin(); it != m_next.end(); it++) @@ -174,7 +217,14 @@ namespace mtconnect { bind(xform); return; } + /// @brief Binds to the first position in the next list + /// @param xform the transform void firstAfter(TransformPtr xform) { m_next.emplace_front(xform); } + /// @brief Replace one transform with another + /// + /// Rebinds the new transform replacing the old transform + /// @param old the transform to be replaced + /// @param xform the new transform void replace(TransformPtr old, TransformPtr xform) { for (auto it = m_next.begin(); it != m_next.end(); it++) @@ -190,6 +240,10 @@ namespace mtconnect { } } + /// @brief remove a transform from the list of next + /// + /// Connects the this transform to the old transforms next transforms + /// @param[in] the transform to remove void remove(TransformPtr old) { for (auto it = m_next.begin(); it != m_next.end(); it++) @@ -212,6 +266,7 @@ namespace mtconnect { Guard m_guard; }; + /// @brief A transform that just returns the entity. It does not call next. class AGENT_LIB_API NullTransform : public Transform { public: diff --git a/src/mtconnect/pipeline/upcase_value.hpp b/src/mtconnect/pipeline/upcase_value.hpp index baa7cb36..a860803a 100644 --- a/src/mtconnect/pipeline/upcase_value.hpp +++ b/src/mtconnect/pipeline/upcase_value.hpp @@ -26,39 +26,35 @@ #include "timestamp_extractor.hpp" #include "transform.hpp" -namespace mtconnect { - class Device; - - namespace pipeline { - inline static std::string &upcase(std::string &s) +namespace mtconnect::pipeline { + inline static std::string &upcase(std::string &s) + { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) -> unsigned char { return std::toupper(c); }); + return s; + } + + /// @brief A simple transform that translates a string to upper case + class AGENT_LIB_API UpcaseValue : public Transform + { + public: + UpcaseValue(const UpcaseValue &) = default; + UpcaseValue() : Transform("UpcaseValue") { - std::transform(s.begin(), s.end(), s.begin(), - [](unsigned char c) -> unsigned char { return std::toupper(c); }); - return s; + using namespace observation; + m_guard = ExactTypeGuard(RUN) || TypeGuard(SKIP); } - class AGENT_LIB_API UpcaseValue : public Transform + const EntityPtr operator()(const EntityPtr entity) override { - public: - UpcaseValue(const UpcaseValue &) = default; - UpcaseValue() : Transform("UpcaseValue") - { - using namespace observation; - m_guard = ExactTypeGuard(RUN) || TypeGuard(SKIP); - } - - const EntityPtr operator()(const EntityPtr entity) override - { - using namespace observation; - auto event = std::dynamic_pointer_cast(entity); - if (!entity) - throw EntityError("Unexpected Entity type in UpcaseValue: ", entity->getName()); - auto nos = std::make_shared(*event.get()); - - upcase(std::get(nos->getValue())); - return next(nos); - } - }; - - } // namespace pipeline -} // namespace mtconnect + using namespace observation; + auto event = std::dynamic_pointer_cast(entity); + if (!entity) + throw EntityError("Unexpected Entity type in UpcaseValue: ", entity->getName()); + auto nos = std::make_shared(*event.get()); + + upcase(std::get(nos->getValue())); + return next(nos); + } + }; +} // namespace mtconnect::pipeline diff --git a/src/mtconnect/printer/json_printer.hpp b/src/mtconnect/printer/json_printer.hpp index 2179715c..8b417e09 100644 --- a/src/mtconnect/printer/json_printer.hpp +++ b/src/mtconnect/printer/json_printer.hpp @@ -23,6 +23,7 @@ #include "mtconnect/utilities.hpp" namespace mtconnect::printer { + /// @brief Printer to generate JSON Documents class AGENT_LIB_API JsonPrinter : public Printer { public: diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index aeb1929b..56810ba6 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -37,47 +37,95 @@ namespace mtconnect { class CuttingTool; } // namespace asset + /// @brief MTConnect abstract document generation namespace namespace printer { using DevicePtr = std::shared_ptr; using ProtoErrorList = std::list>; + /// @brief Abstract document generator interface class AGENT_LIB_API Printer { public: + /// @brief construct a printer + /// @param pretty `true` if content should be pretty printed Printer(bool pretty = false) : m_pretty(pretty) {} virtual ~Printer() = default; + /// @brief Generate an MTConnect Error document + /// @param[in] instanceId the instance id + /// @param[in] bufferSize the buffer size + /// @param[in] nextSeq the next sequence + /// @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) const { return printErrors(instanceId, bufferSize, nextSeq, {{errorCode, errorText}}); } + /// @brief Generate an MTConnect Error document with a error list + /// @param[in] instanceId the instance id + /// @param[in] bufferSize the buffer size + /// @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) const = 0; - + /// @brief Generate an MTConnect Devices document + /// @param[in] instanceId the instance id + /// @param[in] bufferSize the buffer size + /// @param[in] nextSeq the next sequence + /// @param[in] assetBufferSize the asset buffer size + /// @param[in] assetCount the asset count + /// @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) const = 0; - + /// @brief Print a MTConnect Streams document + /// @param[in] instanceId the instance id + /// @param[in] bufferSize the buffer size + /// @param[in] nextSeq the next sequence + /// @param[in] firstSeq the first sequence + /// @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) 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) const = 0; + /// @brief get the mime type for the documents + /// @return the mime type virtual std::string mimeType() const = 0; - + /// @brief Set the last model change time + /// @param t the time void setModelChangeTime(const std::string &t) { m_modelChangeTime = t; } + /// @brief Get the last model change time + /// @return the time const std::string &getModelChangeTime() { return m_modelChangeTime; } + /// @brief set the schema version we are generating + /// @param s the version void setSchemaVersion(const std::string &s) { m_schemaVersion = s; } + /// @brief Get the schema version + /// @return the schema version const auto &getSchemaVersion() const { return m_schemaVersion; } + /// @brief Use the agent version to default the schema version void defaultSchemaVersion() const { if (!m_schemaVersion) diff --git a/src/mtconnect/printer/xml_helper.hpp b/src/mtconnect/printer/xml_helper.hpp index 8f3de51e..94eac2d2 100644 --- a/src/mtconnect/printer/xml_helper.hpp +++ b/src/mtconnect/printer/xml_helper.hpp @@ -22,11 +22,17 @@ #include "mtconnect/config.hpp" #define xml_strfy(line) #line +/// @brief macro to throw an error from XML parsing based on the result of a libxml2 function +/// returning an int +/// @param expr the expression #define THROW_IF_XML2_ERROR(expr) \ if ((expr) < 0) \ { \ throw XmlError("XML Error at " __FILE__ "(" xml_strfy(__LINE__) "): " #expr); \ } +/// @brief macro to throw an error from XML parsing based on the result of a libxml2 function +/// returning a pointer +/// @param expr the expression #define THROW_IF_XML2_NULL(expr) \ if (!(expr)) \ { \ @@ -34,6 +40,7 @@ } namespace mtconnect::printer { + /// @brief Error class for catching parsing errors class AGENT_LIB_API XmlError : public std::logic_error { public: diff --git a/src/mtconnect/printer/xml_printer.hpp b/src/mtconnect/printer/xml_printer.hpp index 50cfacc3..955dcf90 100644 --- a/src/mtconnect/printer/xml_printer.hpp +++ b/src/mtconnect/printer/xml_printer.hpp @@ -36,6 +36,7 @@ namespace mtconnect { namespace printer { class XmlWriter; + /// @brief Printer to generate XML Documents class AGENT_LIB_API XmlPrinter : public Printer { public: @@ -59,34 +60,89 @@ namespace mtconnect { const asset::AssetList &asset) const override; std::string mimeType() const override { return "text/xml"; } + /// @brief Add a Devices XML device namespace + /// @param urn the namespace URN + /// @param location the location of the schema file + /// @param prefix the namespace prefix void addDevicesNamespace(const std::string &urn, const std::string &location, const std::string &prefix); + /// @brief Add a Error XML device namespace + /// @param urn the namespace URN + /// @param location the location of the schema file + /// @param prefix the namespace prefix void addErrorNamespace(const std::string &urn, const std::string &location, const std::string &prefix); + /// @brief Add a Streams XML device namespace + /// @param urn the namespace URN + /// @param location the location of the schema file + /// @param prefix the namespace prefix void addStreamsNamespace(const std::string &urn, const std::string &location, const std::string &prefix); + /// @brief Add a Assets XML device namespace + /// @param urn the namespace URN + /// @param location the location of the schema file + /// @param prefix the namespace prefix void addAssetsNamespace(const std::string &urn, const std::string &location, const std::string &prefix); + /// @brief Set the Devices style sheet to add as a processing instruction + /// @param style the stype sheet void setDevicesStyle(const std::string &style); + /// @brief Set the Streams style sheet to add as a processing instruction + /// @param style the stype sheet void setStreamStyle(const std::string &style); + /// @brief Set the Assets style sheet to add as a processing instruction + /// @param style the stype sheet void setAssetsStyle(const std::string &style); + /// @brief Set the Error style sheet to add as a processing instruction + /// @param style the stype sheet void setErrorStyle(const std::string &style); - // For testing + /// @name For testing + ///@{ + + /// @brief remove all Devices namespaces void clearDevicesNamespaces(); + /// @brief remove all Error namespaces void clearErrorNamespaces(); + /// @brief remove all Streams namespaces void clearStreamsNamespaces(); + /// @brief remove all Assets namespaces void clearAssetsNamespaces(); + ///@} + /// @brief Get the Devices URN for a prefix + /// @param[in] prefix the prefix + /// @return the URN std::string getDevicesUrn(const std::string &prefix); + /// @brief Get the Error URN for a prefix + /// @param[in] prefix the prefix + /// @return the URN std::string getErrorUrn(const std::string &prefix); + /// @brief Get the Streams URN for a prefix + /// @param[in] prefix the prefix + /// @return the URN std::string getStreamsUrn(const std::string &prefix); + /// @brief Get the Assets URN for a prefix + /// @param[in] prefix the prefix + /// @return the URN std::string getAssetsUrn(const std::string &prefix); + /// @brief Get the Devices location for a prefix + /// @param[in] prefix the prefix + /// @return the location std::string getDevicesLocation(const std::string &prefix); + /// @brief Get the Error location for a prefix + /// @param[in] prefix the prefix + /// @return the location std::string getErrorLocation(const std::string &prefix); + /// @brief Get the Streams location for a prefix + /// @param[in] prefix the prefix + /// @return the location std::string getStreamsLocation(const std::string &prefix); + /// @brief Get the Assets location for a prefix + /// @param[in] prefix the prefix + /// @return the location std::string getAssetsLocation(const std::string &prefix); protected: diff --git a/src/mtconnect/printer/xml_printer_helper.hpp b/src/mtconnect/printer/xml_printer_helper.hpp index 826c8032..64eab3aa 100644 --- a/src/mtconnect/printer/xml_printer_helper.hpp +++ b/src/mtconnect/printer/xml_printer_helper.hpp @@ -23,9 +23,12 @@ #include "mtconnect/printer/xml_helper.hpp" namespace mtconnect::printer { + /// @brief Helper class for XML document generation. Wraps some common libxml2 functions class AGENT_LIB_API XmlWriter { public: + /// @brief Construct an XmlWriter creating setting up the buffer for writing. + /// @param pretty `true` if output is formatted with indentation XmlWriter(bool pretty) : m_writer(nullptr), m_buf(nullptr) { THROW_IF_XML2_NULL(m_buf = xmlBufferCreate()); @@ -51,8 +54,12 @@ namespace mtconnect::printer { } } + /// @brief cast this object as a xmlTextWriterPtr + /// @return the xmlTextWriterPtr operator xmlTextWriterPtr() { return m_writer; } + /// @brief Get the content of the buffer as a string. Free the writer if it is allocated. + /// @return content as a string std::string getContent() { if (m_writer != nullptr) @@ -69,30 +76,50 @@ namespace mtconnect::printer { xmlBufferPtr m_buf; }; + /// @brief Wrapper to create an XML open element + /// @param writer the writer + /// @param name the name of the element static inline void openElement(xmlTextWriterPtr writer, const char *name) { THROW_IF_XML2_ERROR(xmlTextWriterStartElement(writer, BAD_CAST name)); } + /// @brief Close the last open element + /// @param writer the writer static inline void closeElement(xmlTextWriterPtr writer) { THROW_IF_XML2_ERROR(xmlTextWriterEndElement(writer)); } + /// @brief Helper class to automatically close an element when the object goes out of scope class AGENT_LIB_API AutoElement { public: + /// @brief Constructor where the element name will be filled in later + /// @param writer the writer AutoElement(xmlTextWriterPtr writer) : m_writer(writer) {} + /// @brief Constor where the element is opened + /// @param writer the writer + /// @param name name of the element + /// @param key optional key if the for closing an element and reopening another element AutoElement(xmlTextWriterPtr writer, const char *name, std::string key = "") : m_writer(writer), m_name(name), m_key(std::move(key)) { openElement(writer, name); } + /// @brief Constor where the element is opened + /// @param writer the writer + /// @param name name of the element + /// @param key optional key if the for closing an element and reopening another element AutoElement(xmlTextWriterPtr writer, const std::string &name, std::string key = "") : m_writer(writer), m_name(name), m_key(std::move(key)) { openElement(writer, name.c_str()); } + /// @brief close the currently open element if the name or the key don't match + /// @param name of the element + /// @param key optional key if the for closing an element and reopening another element + /// @return `true` if the element was closed and reopened bool reset(const std::string &name, const std::string &key = "") { if (name != m_name || m_key != key) @@ -110,13 +137,18 @@ namespace mtconnect::printer { return false; } } + /// @brief Destructor closes the element if it is open ~AutoElement() { if (!m_name.empty()) xmlTextWriterEndElement(m_writer); } + /// @brief get the key + /// @return the key const std::string &key() const { return m_key; } + /// @brief return the name + /// @return the name const std::string &name() const { return m_name; } protected: diff --git a/src/mtconnect/ruby/ruby_agent.hpp b/src/mtconnect/ruby/ruby_agent.hpp index 2e171e18..0f0eac19 100644 --- a/src/mtconnect/ruby/ruby_agent.hpp +++ b/src/mtconnect/ruby/ruby_agent.hpp @@ -125,7 +125,7 @@ namespace mtconnect::ruby { mrb, agentClass, "default_device", [](mrb_state *mrb, mrb_value self) { auto agent = MRubyPtr::unwrap(mrb, self); - auto dev = agent->defaultDevice(); + auto dev = agent->getDefaultDevice(); auto mod = mrb_module_get(mrb, "MTConnect"); auto klass = mrb_class_get_under(mrb, mod, "Device"); diff --git a/src/mtconnect/sink/rest_sink/cached_file.hpp b/src/mtconnect/sink/rest_sink/cached_file.hpp index 1d8220c1..417c16e8 100644 --- a/src/mtconnect/sink/rest_sink/cached_file.hpp +++ b/src/mtconnect/sink/rest_sink/cached_file.hpp @@ -25,94 +25,114 @@ #include "mtconnect/config.hpp" #include "mtconnect/utilities.hpp" -namespace mtconnect { - namespace sink { - namespace rest_sink { - struct CachedFile; - using CachedFilePtr = std::shared_ptr; - struct CachedFile : public std::enable_shared_from_this - { - // Small file size - static const int SMALL_FILE = 10 * 1024; // 10k is considered small +namespace mtconnect::sink::rest_sink { + struct CachedFile; + using CachedFilePtr = std::shared_ptr; + /// @brief A wrapper around a singe cached file that (< 10k) that is served from memory. Also + /// supports files dynamically read from the operating system. + struct CachedFile : public std::enable_shared_from_this + { + // Small file size + static const int SMALL_FILE = 10 * 1024; // 10k is considered small - CachedFile() : m_buffer(nullptr) {} - ~CachedFile() - { - if (m_buffer != nullptr) - free(m_buffer); - } - CachedFilePtr getptr() { return shared_from_this(); } + /// @brief Create a cached file with no buffer + CachedFile() : m_buffer(nullptr) {} + ~CachedFile() + { + if (m_buffer != nullptr) + free(m_buffer); + } + /// @brief get the shared pointer to this file + /// @return shared pointer to this + CachedFilePtr getptr() { return shared_from_this(); } - CachedFile(const CachedFile &file, const std::string &mime) - : m_size(file.m_size), - m_mimeType(mime), - m_path(file.m_path), - m_cached(file.m_cached), - m_lastWrite(file.m_lastWrite) - { - if (m_cached) - { - allocate(file.m_size); - std::memcpy(m_buffer, file.m_buffer, file.m_size); - } - } + /// @brief Create a cached file from another file specifying the mime type + /// @param file the file to copy + /// @param mime the new mime type + CachedFile(const CachedFile &file, const std::string &mime) + : m_size(file.m_size), + m_mimeType(mime), + m_path(file.m_path), + m_cached(file.m_cached), + m_lastWrite(file.m_lastWrite) + { + if (m_cached) + { + allocate(file.m_size); + std::memcpy(m_buffer, file.m_buffer, file.m_size); + } + } - CachedFile(const char *buffer, size_t size, const std::string &mime) - : m_buffer(nullptr), m_size(size), m_mimeType(mime) - { - allocate(m_size); - std::memcpy(m_buffer, buffer, m_size); - } + /// @brief Create a cached file from a buffer and a mime type + /// @param buffer a pointer to the buffer + /// @param size the buffer size + /// @param mime the mime type + CachedFile(const char *buffer, size_t size, const std::string &mime) + : m_buffer(nullptr), m_size(size), m_mimeType(mime) + { + allocate(m_size); + std::memcpy(m_buffer, buffer, m_size); + } - CachedFile(size_t size) : m_buffer(nullptr), m_size(size) { allocate(m_size); } + /// @brief Create a cached file with an fixed size + /// @param size the buffer size + CachedFile(size_t size) : m_buffer(nullptr), m_size(size) { allocate(m_size); } - CachedFile(const std::filesystem::path &path, const std::string &mime, bool cached = true, - size_t size = 0) - : m_buffer(nullptr), m_mimeType(mime), m_path(path), m_cached(cached) - { - if (size == 0) - m_size = std::filesystem::file_size(path); - else - m_size = size; - if (cached) - { - allocate(m_size); - auto file = std::fopen(path.string().c_str(), "rb"); - m_size = std::fread(m_buffer, 1, m_size, file); - } - m_lastWrite = std::filesystem::last_write_time(m_path); - } + /// @brief Create a cahed file by loading the file from the file system + /// @param path the path to the file + /// @param mime the mime type of the file + /// @param cached `true` if the buffer should be allocated + /// @param size optional size; if 0, size of will be determined from the operating system + CachedFile(const std::filesystem::path &path, const std::string &mime, bool cached = true, + size_t size = 0) + : m_buffer(nullptr), m_mimeType(mime), m_path(path), m_cached(cached) + { + if (size == 0) + m_size = std::filesystem::file_size(path); + else + m_size = size; + if (cached) + { + allocate(m_size); + auto file = std::fopen(path.string().c_str(), "rb"); + m_size = std::fread(m_buffer, 1, m_size, file); + } + m_lastWrite = std::filesystem::last_write_time(m_path); + } - CachedFile &operator=(const CachedFile &file) - { - m_cached = file.m_cached; - m_path = file.m_path; - if (m_cached) - { - allocate(file.m_size); - std::memcpy(m_buffer, file.m_buffer, m_size); - } - return *this; - } + /// @brief Clone a CachedFile + /// @param file the file + /// @return this + CachedFile &operator=(const CachedFile &file) + { + m_cached = file.m_cached; + m_path = file.m_path; + if (m_cached) + { + allocate(file.m_size); + std::memcpy(m_buffer, file.m_buffer, m_size); + } + return *this; + } - void allocate(size_t size) - { - if (m_buffer != nullptr) - free(m_buffer); - m_size = size; - m_buffer = static_cast(malloc(m_size + 1)); - memset(m_buffer, 0, m_size + 1); - } + /// @brief Allocate a buffer. If a buffer already exists, free it. Zeros the allocated memory. + /// @param size The size to allocate. + void allocate(size_t size) + { + if (m_buffer != nullptr) + free(m_buffer); + m_size = size; + m_buffer = static_cast(malloc(m_size + 1)); + memset(m_buffer, 0, m_size + 1); + } - char *m_buffer; - size_t m_size {0}; - std::string m_mimeType; - std::filesystem::path m_path; - std::optional m_pathGz; - bool m_cached {true}; - std::filesystem::file_time_type m_lastWrite; - std::optional m_redirect; - }; - } // namespace rest_sink - } // namespace sink -} // namespace mtconnect + char *m_buffer {nullptr}; + size_t m_size {0}; + std::string m_mimeType; + std::filesystem::path m_path; + std::optional m_pathGz; + bool m_cached {true}; + std::filesystem::file_time_type m_lastWrite; + std::optional m_redirect; + }; +} // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/file_cache.cpp b/src/mtconnect/sink/rest_sink/file_cache.cpp index 2e443641..b5ae83dd 100644 --- a/src/mtconnect/sink/rest_sink/file_cache.cpp +++ b/src/mtconnect/sink/rest_sink/file_cache.cpp @@ -58,12 +58,6 @@ namespace mtconnect::sink::rest_sink { namespace fs = std::filesystem; - XmlNamespaceList FileCache::registerFiles(const string &uri, const string &pathName, - const string &version) - { - return registerDirectory(uri, pathName, version); - } - // Register a file XmlNamespaceList FileCache::registerDirectory(const string &uri, const string &pathName, const string &version) diff --git a/src/mtconnect/sink/rest_sink/file_cache.hpp b/src/mtconnect/sink/rest_sink/file_cache.hpp index 9a977336..2e89900d 100644 --- a/src/mtconnect/sink/rest_sink/file_cache.hpp +++ b/src/mtconnect/sink/rest_sink/file_cache.hpp @@ -34,33 +34,73 @@ namespace boost { namespace mtconnect::sink::rest_sink { using XmlNamespace = std::pair; using XmlNamespaceList = std::list; + /// @brief Class to manage file caching for the REST service class AGENT_LIB_API FileCache { public: + /// @brief Directory mapping from the server path to the file system using Directory = std::pair>; + /// @brief Create a file cache + /// @param max optional maxumimum size of the cache, defaults to 20k. FileCache(size_t max = 20 * 1024); + /// @brief register files to be served by the agent. + /// @note Cover method for `registerDirectory()`. + /// @param uri the uri to the file + /// @param path the path on the file system + /// @param version schema version when registering MTConnect files + /// @return A namespace list associated with the files XmlNamespaceList registerFiles(const std::string &uri, const std::string &path, - const std::string &version); + const std::string &version) + { + return registerDirectory(uri, path, version); + } + /// @brief register all files in a directory to be served by the agent + /// @param uri the uri to the file + /// @param path the path on the file system + /// @param version schema version when registering MTConnect files + /// @return A namespace list associated with the files XmlNamespaceList registerDirectory(const std::string &uri, const std::string &path, const std::string &version); + /// @brief Register a single file + /// @note Cover method for `registerFile()` with a filesystem path + /// @param uri the uri for the file + /// @param pathName the path of file on the files system as a string + /// @param version the schema version when registering MTConnect files + /// @return an optional XmlNamespace if successful std::optional registerFile(const std::string &uri, const std::string &pathName, const std::string &version) { std::filesystem::path path(pathName); return registerFile(uri, path, version); } + /// @brief Register a single file + /// @param uri the uri for the file + /// @param pathName the std filesystem path of file + /// @param version the schema version when registering MTConnect files + /// @return an optional XmlNamespace if successful std::optional registerFile(const std::string &uri, const std::filesystem::path &path, const std::string &version); + /// @brief get a cached file given a filename and optional encoding + /// @param name the name of the file from the server + /// @param acceptEncoding optional accepted encodings + /// @param context optional context to perform async io + /// @return shared pointer to the cached file CachedFilePtr getFile(const std::string &name, const std::optional acceptEncoding = std::nullopt, boost::asio::io_context *context = nullptr); + /// @brief check if the file is cached + /// @param name the name of the file from the server + /// @return `true` if the file is cached bool hasFile(const std::string &name) const { return (m_fileCache.count(name) > 0) || (m_fileMap.count(name) > 0); } + /// @brief Register an file name extension with a mime type + /// @param ext the extension (will insert a leading dot if one is not provided) + /// @param type the mime type void addMimeType(const std::string &ext, const std::string &type) { std::string s(ext); @@ -68,17 +108,36 @@ namespace mtconnect::sink::rest_sink { s.insert(0, "."); m_mimeTypes[s] = type; } + + /// @brief Add a directory mapping to the local files system + /// @param uri the server uri to the directory + /// @param path the path on the files system + /// @param index the default file to return for the directory if one is not given. For example, + /// `index.html` void addDirectory(const std::string &uri, const std::string &path, const std::string &index); + /// @brief Set the maximum size of the cache + /// @param s the maximum size void setMaxCachedFileSize(size_t s) { m_maxCachedFileSize = s; } + /// @brief Get the maximum size of the file cache + /// @return the maxumum size auto getMaxCachedFileSize() const { return m_maxCachedFileSize; } + /// @brief Set the file size where they are returned compressed + /// + /// Any file larger than the size will be returned gzipped if the user agent supports + /// compression. + /// @param s the mimum size void setMinCompressedFileSize(size_t s) { m_minCompressedFileSize = s; } + /// @brief Get the minimum file size for compression + /// @return the size auto getMinCompressedFileSize() const { return m_minCompressedFileSize; } - // For testing + /// @name Only used for testing + ///@{ + /// @brief clean the file cache void clear() { m_fileCache.clear(); } - + ///@} protected: CachedFilePtr findFileInDirectories(const std::string &name); const std::string &getMimeType(std::string ext) diff --git a/src/mtconnect/sink/rest_sink/parameter.hpp b/src/mtconnect/sink/rest_sink/parameter.hpp index 6113aa56..3fb176d3 100644 --- a/src/mtconnect/sink/rest_sink/parameter.hpp +++ b/src/mtconnect/sink/rest_sink/parameter.hpp @@ -25,30 +25,39 @@ #include "mtconnect/config.hpp" namespace mtconnect::sink::rest_sink { + /// @brief Parameter related errors thrown during interpreting a REST request class AGENT_LIB_API ParameterError : public std::logic_error { using std::logic_error::logic_error; }; + /// @brief The parameter type for a REST request enum ParameterType : uint8_t { - NONE = 0, - STRING = 1, - INTEGER = 2, - UNSIGNED_INTEGER = 3, - DOUBLE = 4 + NONE = 0, ///< No specific type + STRING = 1, ///< A string + INTEGER = 2, ///< A signed integer + UNSIGNED_INTEGER = 3, ///< An unsigned integer + DOUBLE = 4 ///< A double }; + /// @brief The part of the path the parameter is related to enum UrlPart { - PATH, - QUERY + PATH, ///< The portion before the `?` + QUERY ///< The portion after the `?` }; + /// @brief The variant for query parameters using ParameterValue = std::variant; + /// @brief A struct to hold a parameter template for matching portions of a REST URI struct Parameter { Parameter() = default; + /// @brief Create a parameter + /// @param n the name of the parameter + /// @param t the parameter type. defaults to STRING + /// @param p path or query portion of the URI Parameter(const std::string &n, ParameterType t = STRING, UrlPart p = PATH) : m_name(n), m_type(t), m_part(p) {} @@ -56,14 +65,20 @@ namespace mtconnect::sink::rest_sink { std::string m_name; ParameterType m_type {STRING}; + /// @brief Default value if one is available ParameterValue m_default; UrlPart m_part {PATH}; + /// @brief to support std::set interface bool operator<(const Parameter &o) const { return m_name < o.m_name; } }; + /// @brief Ordered list of path parameters as they appear in the URI using ParameterList = std::list; + /// @brief set of query parameters using QuerySet = std::set; + /// @brief Associates a parameter name with a value using ParameterMap = std::map; + /// @brief associates a query parameter with a string value using QueryMap = std::map; } // namespace mtconnect::sink::rest_sink diff --git a/src/mtconnect/sink/rest_sink/request.hpp b/src/mtconnect/sink/rest_sink/request.hpp index 71b0a148..f62f37db 100644 --- a/src/mtconnect/sink/rest_sink/request.hpp +++ b/src/mtconnect/sink/rest_sink/request.hpp @@ -26,10 +26,17 @@ #include "parameter.hpp" namespace mtconnect::sink::rest_sink { + /// @brief An error that occurred during a request class AGENT_LIB_API RequestError : public std::logic_error { public: + /// @brief Create a simple error message related to a request RequestError(const char *w) : std::logic_error::logic_error(w) {} + /// @brief Create a request error + /// @param w the message + /// @param body the body of the request + /// @param type the request type + /// @param code the boost status code RequestError(const char *w, const std::string &body, const std::string &type, boost::beast::http::status code) : std::logic_error::logic_error(w), m_contentType(type), m_body(body), m_code(code) @@ -45,19 +52,26 @@ namespace mtconnect::sink::rest_sink { class Session; using SessionPtr = std::shared_ptr; + /// @brief A wrapper around an incoming HTTP request + /// + /// The request can be a simple reply response or streaming request struct Request { - boost::beast::http::verb m_verb; - std::string m_body; - std::string m_accepts; - std::string m_acceptsEncoding; - std::string m_contentType; - std::string m_path; - std::string m_foreignIp; - uint16_t m_foreignPort; - QueryMap m_query; - ParameterMap m_parameters; + 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 + std::string m_acceptsEncoding; ///< Encodings that can be returned + std::string m_contentType; ///< The content type for the body + std::string m_path; ///< The URI for the request + std::string m_foreignIp; ///< The requestors IP Address + uint16_t m_foreignPort; ///< The requestors Port + QueryMap m_query; ///< The parsed query parameters + ParameterMap m_parameters; ///< The parsed path parameters + /// @brief Find a parameter by type + /// @tparam T the type of the parameter + /// @param s the name of the parameter + /// @return an option type `T` if the parameter is found template std::optional parameter(const std::string &s) const { diff --git a/src/mtconnect/sink/rest_sink/response.hpp b/src/mtconnect/sink/rest_sink/response.hpp index 2e095410..f8ae3c98 100644 --- a/src/mtconnect/sink/rest_sink/response.hpp +++ b/src/mtconnect/sink/rest_sink/response.hpp @@ -35,26 +35,37 @@ namespace mtconnect { namespace sink::rest_sink { using status = boost::beast::http::status; + /// @brief A response for a simple request request returning some content struct Response { + /// @brief Create a response with a status and a body + /// @param[in] status the status + /// @param[in] body the body of the response + /// @param[in] mimeType the mime type of the response Response(status status = status::ok, const std::string &body = "", const std::string &mimeType = "text/xml") : m_status(status), m_body(body), m_mimeType(mimeType), m_expires(0) {} + /// @brief Create a response with a status and a cached file + /// @param[in] status the status of the response + /// @param[in] file the file Response(status status, CachedFilePtr file) : m_status(status), m_mimeType(file->m_mimeType), m_expires(0), m_file(file) {} + /// @brief Creates a response with an error + /// @param[in] e the error Response(RequestError &e) : m_status(e.m_code), m_body(e.m_body), m_mimeType(e.m_contentType) {} - status m_status; - std::string m_body; - std::string m_mimeType; - std::optional m_location; - std::chrono::seconds m_expires; - bool m_close {false}; + status m_status; ///< The return status + std::string m_body; ///< The body of the response + std::string m_mimeType; ///< The mime type of the response + std::optional m_location; ///< optional location + 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 - CachedFilePtr m_file; + CachedFilePtr m_file; ///< Cached file if a file is being returned }; using ResponsePtr = std::unique_ptr; diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 5f129788..ae0839e0 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -117,33 +117,6 @@ namespace mtconnect { void RestService::stop() { m_server->stop(); } - // Observation management - observation::ObservationPtr RestService::getFromBuffer(uint64_t seq) const - { - return m_sinkContract->getCircularBuffer().getFromBuffer(seq); - } - - SequenceNumber_t RestService::getSequence() const - { - return m_sinkContract->getCircularBuffer().getSequence(); - } - - unsigned int RestService::getBufferSize() const - { - return m_sinkContract->getCircularBuffer().getBufferSize(); - } - - SequenceNumber_t RestService::getFirstSequence() const - { - return m_sinkContract->getCircularBuffer().getFirstSequence(); - } - - // For testing... - void RestService::setSequence(uint64_t seq) - { - m_sinkContract->getCircularBuffer().setSequence(seq); - } - // Configuration void RestService::loadNamespace(const ptree &tree, const char *namespaceType, XmlPrinter *xmlPrinter, NamespaceFunction callback) @@ -769,7 +742,7 @@ namespace mtconnect { asyncResponse->m_printer = printer; asyncResponse->m_heartbeat = std::chrono::milliseconds(heartbeatIn); asyncResponse->m_service = getptr(); - + checkPath(asyncResponse->m_printer, path, dev, asyncResponse->m_filter); if (m_logStreamData) @@ -786,7 +759,8 @@ namespace mtconnect { m_sinkContract->getDataItemById(item)->addObserver(&asyncResponse->m_observer); chrono::milliseconds interMilli {interval}; - SequenceNumber_t firstSeq = getFirstSequence(); + SequenceNumber_t firstSeq = m_sinkContract->getCircularBuffer().getFirstSequence(); + ; if (!from || *from < firstSeq) asyncResponse->m_sequence = firstSeq; else @@ -832,7 +806,7 @@ namespace mtconnect { using boost::placeholders::_2; auto service = asyncResponse->m_service.lock(); - + if (!service || !m_server || !m_server->isRunning()) { LOG(warning) << "Trying to send chunk when service has stopped"; @@ -904,7 +878,7 @@ namespace mtconnect { // Check if we're falling too far behind. If we are, generate an // MTConnectError and return. - if (asyncResponse->m_sequence < getFirstSequence()) + if (asyncResponse->m_sequence < m_sinkContract->getCircularBuffer().getFirstSequence()) { LOG(warning) << "Client fell too far behind, disconnecting"; asyncResponse->m_session->fail(boost::beast::http::status::not_found, @@ -946,7 +920,7 @@ namespace mtconnect { AsyncCurrentResponse(rest_sink::SessionPtr session, asio::io_context &context) : m_session(session), m_timer(context) {} - + std::weak_ptr m_service; rest_sink::SessionPtr m_session; chrono::milliseconds m_interval; @@ -989,7 +963,7 @@ namespace mtconnect { using boost::placeholders::_1; auto service = asyncResponse->m_service.lock(); - + if (!service || !m_server || !m_server->isRunning()) { LOG(warning) << "Trying to send chunk when service has stopped"; @@ -1085,7 +1059,7 @@ namespace mtconnect { if (device) dev = checkDevice(printer, *device); else - dev = m_sinkContract->defaultDevice(); + dev = m_sinkContract->getDefaultDevice(); auto ap = m_loopback->receiveAsset(dev, asset, uuid, type, nullopt, errors); if (!ap || errors.size() > 0 || (type && ap->getType() != *type)) { @@ -1343,7 +1317,7 @@ namespace mtconnect { { std::lock_guard lock(m_sinkContract->getCircularBuffer()); - firstSeq = getFirstSequence(); + firstSeq = m_sinkContract->getCircularBuffer().getFirstSequence(); seq = m_sinkContract->getCircularBuffer().getSequence(); if (at) { @@ -1373,7 +1347,7 @@ namespace mtconnect { { std::lock_guard lock(m_sinkContract->getCircularBuffer()); - firstSeq = getFirstSequence(); + firstSeq = m_sinkContract->getCircularBuffer().getFirstSequence(); auto seq = m_sinkContract->getCircularBuffer().getSequence(); lastSeq = seq - 1; int upperCountLimit = m_sinkContract->getCircularBuffer().getBufferSize() + 1; diff --git a/src/mtconnect/sink/rest_sink/rest_service.hpp b/src/mtconnect/sink/rest_sink/rest_service.hpp index 7980955a..2368641a 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.hpp +++ b/src/mtconnect/sink/rest_sink/rest_service.hpp @@ -33,30 +33,44 @@ namespace mtconnect { class XmlPrinter; } + /// @brief MTConnect REST normative implemention namespace namespace sink::rest_sink { struct AsyncSampleResponse; struct AsyncCurrentResponse; + /// @brief Callback fundtion for setting namespaces using NamespaceFunction = void (printer::XmlPrinter::*)(const std::string &, const std::string &, const std::string &); + /// @brief Callback fundtion for setting stylesheet using StyleFunction = void (printer::XmlPrinter::*)(const std::string &); + /// @brief The Sink for the MTConnect normative REST Service class AGENT_LIB_API RestService : public Sink { public: + /// @brief Create a Rest 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 RestService(boost::asio::io_context &context, SinkContractPtr &&contract, const ConfigOptions &options, const boost::property_tree::ptree &config); ~RestService() = default; - // Register the service with the sink factory + /// @brief Register the Sink factory to create this sink + /// @param factory static void registerFactory(SinkFactory &factory); + /// @brief Make a loopback source to handle PUT, POST, and DELETE + /// @param context the pipeline context + /// @return shared pointer to a loopback source std::shared_ptr makeLoopbackSource( pipeline::PipelineContextPtr context); - // Sink Methods + /// @name Interface methods + ///@{ void start() override; void stop() override; @@ -64,94 +78,177 @@ namespace mtconnect { bool publish(observation::ObservationPtr &observation) override; bool publish(asset::AssetPtr asset) override { return false; } + ///@} + /// @brief Get the HTTP server + /// @return pointer to the HTTP server auto getServer() { return m_server.get(); } - + /// @brief Get the file cache + /// @return pointer to the file cache auto getFileCache() { return &m_fileCache; } - // Observation management - observation::ObservationPtr getFromBuffer(uint64_t seq) const; - - SequenceNumber_t getSequence() const; - - unsigned int getBufferSize() const; + /// @name MTConnect Request Handlers + ///@{ - SequenceNumber_t getFirstSequence() const; - - // For testing... - void setSequence(uint64_t seq); - - // MTConnect Requests - ResponsePtr probeRequest(const printer::Printer *, + /// @brief Handler for a probe request + /// @param p printer for doc generation + /// @param device optional device name or uuid + /// @return MTConnect Devices response + ResponsePtr probeRequest(const printer::Printer *p, const std::optional &device = std::nullopt); - ResponsePtr currentRequest(const printer::Printer *, + /// @brief Handler for a current request + /// @param p printer for doc generation + /// @param device optional device name or uuid + /// @param at optional sequence number to take the snapshot + /// @param path an xpath to filter + /// @return MTConnect Streams response + ResponsePtr currentRequest(const printer::Printer *p, const std::optional &device = std::nullopt, const std::optional &at = std::nullopt, const std::optional &path = std::nullopt); - ResponsePtr sampleRequest(const printer::Printer *, const int count = 100, + /// @brief Handler for a sample request + /// @param p printer for doc generation + /// @param count maximum number of observations + /// @param device optional device name or uuid + /// @param from optional starting sequence number + /// @param to optional ending sequence number + /// @param path an xpath for filtering + /// @return MTConnect Streams response + ResponsePtr sampleRequest(const printer::Printer *p, const int count = 100, const std::optional &device = std::nullopt, const std::optional &from = std::nullopt, const std::optional &to = std::nullopt, const std::optional &path = std::nullopt); - - void streamSampleRequest(SessionPtr session, const printer::Printer *, const int interval, + /// @brief Handler for a streaming sample + /// @param session session to stream data to + /// @param p printer for doc generation + /// @param interval the minimum interval between sending documents in ms + /// @param heartbeat how often to send an empty document if no activity in ms + /// @param count the maxumum number of observations + /// @param device optional device name or uuid + /// @param from optional starting sequence number + /// @param path optional path for filtering + void streamSampleRequest(SessionPtr session, const printer::Printer *p, const int interval, const int heartbeat, const int count = 100, const std::optional &device = std::nullopt, const std::optional &from = std::nullopt, const std::optional &path = std::nullopt); - // Async stream method + /// @brief Handler for a streaming current + /// @param session session to stream data to + /// @param p printer for doc generation + /// @param interval the minimum interval between sending documents in ms + /// @param device optional device name or uuid + /// @param path optional path for filtering + void streamCurrentRequest(SessionPtr session, const printer::Printer *p, const int interval, + const std::optional &device = std::nullopt, + const std::optional &path = std::nullopt); + /// @brief Handler for put/post observation + /// @param p printer for response generation + /// @param device device + /// @param observations key/value pairs for the observations + /// @param time optional timestamp + /// @return `` if succeeds + ResponsePtr putObservationRequest(const printer::Printer *p, const std::string &device, + const QueryMap observations, + const std::optional &time = std::nullopt); + + ///@} + + /// @name Async stream method + ///@{ + + /// @brief Callback when the async write completes + /// @param asyncResponse shared pointer to response referencing the session void streamSampleWriteComplete(std::shared_ptr asyncResponse); + /// @brief After the write complete, send the next chunk of data + /// @param asyncResponse shared pointer to async response referencing the session + /// @param ec an async error code void streamNextSampleChunk(std::shared_ptr asyncResponse, boost::system::error_code ec); - void streamCurrentRequest(SessionPtr session, const printer::Printer *, const int interval, - const std::optional &device = std::nullopt, - const std::optional &path = std::nullopt); - + /// @brief Callback to stream another current chunk + /// @param asyncResponse shared pointer to async response referencing the session + /// @param ec an async error code void streamNextCurrent(std::shared_ptr asyncResponse, boost::system::error_code ec); - - // Asset requests - ResponsePtr assetRequest(const printer::Printer *, const int32_t count, const bool removed, + ///@} + + /// @name Asset Request Handler + ///@{ + + /// @brief Asset request handler for assets by type or device + /// @param p printer for the response document + /// @param count maximum number of assets to return + /// @param removed `true` if response should include removed assets + /// @param type optional type of asset to filter + /// @param device optional device name or uuid + /// @return MTConnect Assets response document + 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); - ResponsePtr assetIdsRequest(const printer::Printer *, const std::list &ids); - - ResponsePtr putAssetRequest(const printer::Printer *, const std::string &asset, + /// @brief Asset request handler using a list of asset ids + /// @param p printer for the response document + /// @param ids list of asset ids + /// @return MTConnect Assets response document + ResponsePtr assetIdsRequest(const printer::Printer *p, const std::list &ids); + + /// @brief Asset request handler to update an asset + /// @param p printer for the response document + /// @param asset the asset body + /// @param type optional type, if not given will derive from `asset` + /// @param device option device, if not given will derive from `asset` + /// @param uuid optional asset id, if not given will derive from the `asset` + /// @return MTConnect Assets response document + ResponsePtr putAssetRequest(const printer::Printer *p, const std::string &asset, const std::optional &type, const std::optional &device = std::nullopt, const std::optional &uuid = std::nullopt); - - ResponsePtr deleteAssetRequest(const printer::Printer *, const std::list &ids); - - ResponsePtr deleteAllAssetsRequest(const printer::Printer *, + /// @brief Asset request handler to delete a a list of asset ids + /// @param p printer for the response document + /// @param ids the list of ids + /// @return MTConnect Assets response document + ResponsePtr deleteAssetRequest(const printer::Printer *p, const std::list &ids); + /// @brief Asset request handler to delete all assets by device and/or type + /// @param p printer for the response document + /// @param device optional device + /// @param type optonal type + /// @return number of assets removed as response + ResponsePtr deleteAllAssetsRequest(const printer::Printer *p, const std::optional &device = std::nullopt, const std::optional &type = std::nullopt); + ///@} - ResponsePtr putObservationRequest(const printer::Printer *, const std::string &device, - const QueryMap observations, - const std::optional &time = std::nullopt); - - // For debugging + /// @brief For debugging: turn on stream data logging + /// @note This is only for debuging void setLogStreamData(bool log); - // Get the printer for a type + /// @brief Check the accepts header for a matching printer key + /// @param accepts the accepts header + /// @return printer key or `xml` if one is not found const std::string acceptFormat(const std::string &accepts) const; - + /// @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; - // Output an XML Error + /// @brief Generate an MTConnect Error document + /// @param printer printer to generate error + /// @param errorCode an error code + /// @param text descriptive error text + /// @return MTConnect Error document std::string printError(const printer::Printer *printer, const std::string &errorCode, const std::string &text) const; - // For testing + /// @name For testing only + ///@{ auto instanceId() const { return m_instanceId; } void setInstanceId(uint64_t id) { m_instanceId = id; } + ///@} protected: // Configuration diff --git a/src/mtconnect/sink/rest_sink/routing.hpp b/src/mtconnect/sink/rest_sink/routing.hpp index 9bd1ac5f..2b78e418 100644 --- a/src/mtconnect/sink/rest_sink/routing.hpp +++ b/src/mtconnect/sink/rest_sink/routing.hpp @@ -37,12 +37,20 @@ namespace mtconnect::sink::rest_sink { class Session; using SessionPtr = std::shared_ptr; + /// @brief A REST routing that parses a URI pattern and associates a lambda when it is matched + /// against a request class AGENT_LIB_API Routing { public: using Function = std::function; Routing(const Routing &r) = default; + /// @brief Create a routing + /// + /// Creates a REGEXP to match against the path + /// @param verb The `GET`, `PUT`, `POST`, and `DELETE` version of the HTTP request + /// @param pattern the URI pattern to parse and match + /// @param function the function to call if matches Routing(boost::beast::http::verb verb, const std::string &pattern, const Function function) : m_verb(verb), m_function(function) { @@ -59,13 +67,27 @@ namespace mtconnect::sink::rest_sink { pathParameters(s); } + /// @brief + /// @param verb + /// @param pattern + /// @param function Routing(boost::beast::http::verb verb, const std::regex &pattern, const Function function) : m_verb(verb), m_pattern(pattern), m_function(function) {} + /// @brief Get the list of path position in order + /// @return the parameter list const ParameterList &getPathParameters() const { return m_pathParameters; } + /// @brief get the unordered set of query parameters const QuerySet &getQueryParameters() const { return m_queryParameters; } + /// @brief match the session's request against the this routing + /// + /// Call the associated lambda when matched + /// + /// @param[in] session the session making the request to pass to the Routing if matched + /// @param[in,out] request the incoming request with a verb and a path + /// @return `true` if the request was matched bool matches(SessionPtr session, RequestPtr request) { try diff --git a/src/mtconnect/sink/rest_sink/server.hpp b/src/mtconnect/sink/rest_sink/server.hpp index f6e5005e..cdb15da0 100644 --- a/src/mtconnect/sink/rest_sink/server.hpp +++ b/src/mtconnect/sink/rest_sink/server.hpp @@ -43,9 +43,17 @@ #include "tls_dector.hpp" namespace mtconnect::sink::rest_sink { + /// @brief An HTTP Server for REST class AGENT_LIB_API Server { public: + /// @brief Create an HTTP server with an asio context and options + /// @param context a boost asio context + /// @param options configuration options + /// - Port, defaults to 5000 + /// - AllowPut, defaults to false + /// - ServerIp, defaults to 0.0.0.0 + /// - HttpHeaders Server(boost::asio::io_context &context, const ConfigOptions &options = {}) : m_context(context), m_port(GetOption(options, configuration::Port).value_or(5000)), @@ -76,18 +84,21 @@ namespace mtconnect::sink::rest_sink { loadTlsCertificate(); } - // Start the http server + /// @brief Start the http server void start(); - // Shutdown + /// @brief Shutdown the http server void stop() { m_run = false; m_acceptor.close(); }; + /// @brief Listen for async connections void listen(); + /// @brief Add additional HTTP headers + /// @param[in] fields the header fields as `: ` void setHttpHeaders(const StringList &fields) { for (auto &f : fields) @@ -100,21 +111,48 @@ namespace mtconnect::sink::rest_sink { } } + /// @brief Get the list of header fields + /// @return header fields const auto &getHttpHeaders() const { return m_fields; } - + /// @brief get the bind port + /// @return the port being bound auto getPort() const { return m_port; } - // PUT and POST handling + /// @name PUT and POST handling + ///@{ + + /// @brief is the server listening for new connections + /// @return `true` if it is listening bool isListening() const { return m_listening; } + /// @brief is the server running + /// @return `false` when shutting down bool isRunning() const { return m_run; } + /// @brief are puts allowed? + /// @return `true` if one can put to the server bool arePutsAllowed() const { return m_allowPuts; } + /// @brief can one put from a particular IP address or host + /// @param[in] host the host + /// @return `true` if puts are allowed bool allowPutFrom(const std::string &host); + /// @brief sets the allow puts flag + /// @param[in] allow void allowPuts(bool allow = true) { m_allowPuts = allow; } + /// @brief can one put from an ip address + /// @param[in] addr the ip address + /// @return `true` if puts are accepted from that address bool isPutAllowedFrom(boost::asio::ip::address &addr) const { return m_allowPutsFrom.find(addr) != m_allowPutsFrom.end(); } - + ///@} + + /// @brief Entry point for all requests + /// + /// Search routings for a match, if a match is found, then dispatch the request, otherwise + /// return an error. + /// @param[in] session the client session + /// @param[in] request the incoming request + /// @return `true` if the request was matched and dispatched bool dispatch(SessionPtr session, RequestPtr request) { try @@ -161,14 +199,29 @@ namespace mtconnect::sink::rest_sink { return false; } + /// @brief accept a connection from a client + /// @param[in] ec an error code + /// @param[in] soc the incoming connection socket void accept(boost::system::error_code ec, boost::asio::ip::tcp::socket soc); + /// @brief Method that generates an MTConnect Error document + /// @param[in] ec an error code + /// @param[in] what the description why the request failed void fail(boost::system::error_code ec, char const *what); + /// @brief Add a routing to the server + /// @param[in] routing the routing void addRouting(const Routing &routing) { m_routings.emplace_back(routing); } + /// @brief Set the error function to format the error during failure + /// @param func the error function void setErrorFunction(const ErrorFunction &func) { m_errorFunction = func; } + /// @brief get the error funciton + /// @return the error function ErrorFunction getErrorFunction() const { return m_errorFunction; } - // Callback for testing. Allows test to grab the last session dispatched. + /// @name Only for testing + ///@{ + /// @brief Callback for testing. Allows test to grab the last session dispatched. std::function m_lastSession; + ///@} protected: void loadTlsCertificate(); diff --git a/src/mtconnect/sink/rest_sink/session.hpp b/src/mtconnect/sink/rest_sink/session.hpp index 1cca5c7c..a6443f78 100644 --- a/src/mtconnect/sink/rest_sink/session.hpp +++ b/src/mtconnect/sink/rest_sink/session.hpp @@ -39,19 +39,44 @@ namespace mtconnect::sink::rest_sink { using Complete = std::function; using FieldList = std::list>; + /// @brief An abstract Session for an HTTP connection to a client + /// + /// The HTTP or HTTPS connections are subclasses of the session class Session : public std::enable_shared_from_this { public: + /// @brief Create a session + /// @param dispatch dispatching function to handle the request + /// @param func error function to format the error response Session(Dispatch dispatch, ErrorFunction func) : m_dispatch(dispatch), m_errorFunction(func) {} virtual ~Session() {} + /// @brief start the session virtual void run() = 0; + /// @brief write the response to the client + /// @param response the response + /// @param complete optional completion callback virtual void writeResponse(ResponsePtr &&response, Complete complete = nullptr) = 0; + /// @brief write a failure response to the client + /// @param response the response + /// @param complete optional completion callback virtual void writeFailureResponse(ResponsePtr &&response, Complete complete = nullptr) = 0; + /// @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; + /// @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; + /// @brief close the session virtual void close() = 0; + /// @brief close the stream virtual void closeStream() = 0; + /// @brief Log a failure and close the session + /// @param status the HTTP status + /// @param message the message + /// @param ec an optional error code virtual void fail(boost::beast::http::status status, const std::string &message, boost::system::error_code ec = boost::system::error_code {}) { @@ -69,13 +94,22 @@ namespace mtconnect::sink::rest_sink { } } + /// @brief enable puts for the session + /// @param allow `true` if puts are allowed void allowPuts(bool allow = true) { m_allowPuts = allow; } + /// @brief allow puts from a set of hosts + /// @note also sets allow puts to `true` + /// @param hosts set of hosts void allowPutsFrom(std::set &hosts) { m_allowPuts = true; m_allowPutsFrom = hosts; } - auto getRemote() const { return m_remote; } + /// @brief get the remote endpoint + /// @return the asio tcp endpoint + auto &getRemote() const { return m_remote; } + /// @brief set the request as unauthorized + /// @param msg the rational message void setUnauthorized(const std::string &msg) { m_message = msg; diff --git a/src/mtconnect/sink/rest_sink/session_impl.cpp b/src/mtconnect/sink/rest_sink/session_impl.cpp index e93e6f0b..2f6d6e43 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.cpp +++ b/src/mtconnect/sink/rest_sink/session_impl.cpp @@ -461,9 +461,17 @@ namespace mtconnect::sink::rest_sink { } } + /// @brief A secure https session class HttpsSession : public SessionImpl { public: + /// @brief Create a secure https session + /// @param socket tcp socket (takes ownership) + /// @param context the asio ssl context + /// @param buffer the buffer (takes ownership) + /// @param list the header fieldlist + /// @param dispatch dispatch function + /// @param error error function HttpsSession(boost::beast::tcp_stream &&socket, boost::asio::ssl::context &context, boost::beast::flat_buffer &&buffer, const FieldList &list, Dispatch dispatch, ErrorFunction error) @@ -472,10 +480,13 @@ namespace mtconnect::sink::rest_sink { { m_remote = beast::get_lowest_layer(m_stream).socket().remote_endpoint(); } + /// @brief get a shared pointer to the https session + /// @return shared pointer std::shared_ptr shared_ptr() { return std::dynamic_pointer_cast(shared_from_this()); } + /// @brief close this session virtual ~HttpsSession() { close(); } auto &stream() { return m_stream; } @@ -489,8 +500,11 @@ namespace mtconnect::sink::rest_sink { beast::bind_front_handler(&HttpsSession::handshake, shared_ptr())); } + /// @brief return the stream and hand over ownership + /// @return the stream beast::ssl_stream releaseStream() { return std::move(m_stream); } + /// @brief shutdown the stream asyncronously closing the secure stream void close() override { if (!m_closing) diff --git a/src/mtconnect/sink/rest_sink/session_impl.hpp b/src/mtconnect/sink/rest_sink/session_impl.hpp index f9ca1715..6ab2b982 100644 --- a/src/mtconnect/sink/rest_sink/session_impl.hpp +++ b/src/mtconnect/sink/rest_sink/session_impl.hpp @@ -35,29 +35,44 @@ namespace mtconnect { } namespace sink::rest_sink { + /// @brief A session implementation `Derived` subclass pattern + /// @tparam subclass of this class to use the same methods with http or https protocol streams template class SessionImpl : public Session { public: + /// @brief Create a Session + /// @param buffer buffer (takes ownership) + /// @param list http fields + /// @param dispatch dispatch method + /// @param error error function SessionImpl(boost::beast::flat_buffer &&buffer, const FieldList &list, Dispatch dispatch, ErrorFunction error) : Session(dispatch, error), m_fields(list), m_buffer(std::move(buffer)) {} + /// @brief Sessions cannot be copied SessionImpl(const SessionImpl &) = delete; virtual ~SessionImpl() {} + + /// @brief get a shared pointer to this + /// @return shared session impl std::shared_ptr shared_ptr() { return std::dynamic_pointer_cast(shared_from_this()); } + /// @brief get this as the `Derived` type + /// @return Derived &derived() { return static_cast(*this); } + /// @name Session Interface + ///@{ 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 closeStream() override; - + ///@} protected: template void addHeaders(const Response &response, T &res); @@ -91,23 +106,35 @@ namespace mtconnect { ResponsePtr m_outgoing; }; + /// @brief An HTTP Session for communication without TLS class HttpSession : public SessionImpl { public: + /// @brief Create an http session with a plain tcp stream + /// @param stream the stream (takes ownership) + /// @param buffer the communiction buffer (takes ownership) + /// @param list list of fields + /// @param dispatch dispatch function + /// @param error error format function HttpSession(boost::beast::tcp_stream &&stream, boost::beast::flat_buffer &&buffer, const FieldList &list, Dispatch dispatch, ErrorFunction error) : SessionImpl(move(buffer), list, dispatch, error), m_stream(std::move(stream)) { m_remote = m_stream.socket().remote_endpoint(); } + /// @brief Get a pointer cast as an HTTP Session + /// @return shared pointer to an http session std::shared_ptr shared_ptr() { return std::dynamic_pointer_cast(shared_from_this()); } + /// @brief destruct and close the session virtual ~HttpSession() { close(); } - + /// @brief get the stream + /// @return the stream auto &stream() { return m_stream; } + /// @brief close the session and shutdown the socket void close() override { NAMED_SCOPE("HttpSession::close"); diff --git a/src/mtconnect/sink/rest_sink/tls_dector.hpp b/src/mtconnect/sink/rest_sink/tls_dector.hpp index 1ec7ced2..197e9a91 100644 --- a/src/mtconnect/sink/rest_sink/tls_dector.hpp +++ b/src/mtconnect/sink/rest_sink/tls_dector.hpp @@ -25,9 +25,21 @@ #include "session.hpp" namespace mtconnect::sink::rest_sink { + /// @brief Helper class to detect when a connection is using Transport Layer Security + /// + /// This class checks the protocol and then creates a TLS session or a regular HTTP session. class TlsDector : public std::enable_shared_from_this { public: + /// @brief Create a TLS detector to create the asynconously check for a secure connection + /// @param[in] socket incoming socket connection (takes ownership) + /// @param[in] context asio context + /// @param[in] tlsOnly only allow TLS connects, reject otherwise + /// @param[in] allowPuts allow puts + /// @param[in] allowPutsFrom allow puts from an address + /// @param[in] list the header fields + /// @param[in] dispatch a dispatcher function + /// @param[in] error an error function TlsDector(boost::asio::ip::tcp::socket &&socket, boost::asio::ssl::context &context, bool tlsOnly, bool allowPuts, const std::set &allowPutsFrom, const FieldList &list, Dispatch dispatch, ErrorFunction error) @@ -43,6 +55,9 @@ namespace mtconnect::sink::rest_sink { ~TlsDector() {} + /// @brief Method to call when TLS operation fails + /// @param[in] ec the erro code + /// @param[in] message the message void fail(boost::system::error_code ec, const std::string &message) { NAMED_SCOPE("TlsDector::fail"); @@ -54,8 +69,14 @@ namespace mtconnect::sink::rest_sink { } } + /// @brief ensure the detection is done in the streams executor void run(); + /// @brief asyncronously detect an SSL connection. + /// @note times out after 30 seconds void detect(); + /// @brief the detection async callback + /// @param[in] ec an error code + /// @param[in] isTls `true` if this is a TLS connection void detected(boost::beast::error_code ec, bool isTls); protected: diff --git a/src/mtconnect/sink/sink.hpp b/src/mtconnect/sink/sink.hpp index d7599b35..fc390af5 100644 --- a/src/mtconnect/sink/sink.hpp +++ b/src/mtconnect/sink/sink.hpp @@ -47,39 +47,76 @@ namespace mtconnect { class CircularBuffer; } + /// @brief The Sink namespace for outgoing data from the agent namespace sink { + /// @brief Interface required by sinks class AGENT_LIB_API SinkContract { public: virtual ~SinkContract() = default; + /// @brief get the printer for a mime type. Current options: `xml` or `json`. + /// @param[in] aType a string for the type + /// @return A pointer to a printer for that type. `nullptr` if not found virtual printer::Printer *getPrinter(const std::string &aType) const = 0; + /// @brief get the map of type/printer pairs + /// @return a reference to the map (ownership is not transferred). virtual const PrinterMap &getPrinters() const = 0; - // Get device from device map + /// @brief find a device by name + /// @param[in] name the name of the device + /// @return shared pointer to the device if found virtual DevicePtr getDeviceByName(const std::string &name) const = 0; + /// @brief find a device by its uuid or name + /// @param idOrName the uuid or name + /// @return shared pointer to the device if found virtual DevicePtr findDeviceByUUIDorName(const std::string &idOrName) const = 0; + /// @brief get a list of all the devices + /// @return a list of shared device pointers virtual const std::list getDevices() const = 0; - virtual DevicePtr defaultDevice() const = 0; + /// @brief get the default device + /// @return the default device + /// + /// This is the first device that is not the agent device + virtual DevicePtr getDefaultDevice() const = 0; + /// @brief get a data item by its unique id + /// @param[in] id a unique id + /// @return shared pointer to the data item if found virtual DataItemPtr getDataItemById(const std::string &id) const = 0; + /// @brief find all the data items for a given XPath + /// @param[in] device optional device to search + /// @param[in] path the xpath to search + /// @param[out] filter the set of all data items matching path to use for filtering virtual void getDataItemsForPath(const DevicePtr device, const std::optional &path, FilterSet &filter) const = 0; + /// @brief Add a source for this sink. + /// + /// This is used to create loopback sources for a sink + /// + /// @param source shared pointer to the source virtual void addSource(std::shared_ptr source) = 0; + /// @brief Get the common circular buffer + /// @return a reference to the circular buffer virtual buffer::CircularBuffer &getCircularBuffer() = 0; - // Asset information + /// @brief Get a pointer to the asset storage + /// @return a pointer to the asset storage. virtual const asset::AssetStorage *getAssetStorage() = 0; + /// @brief Shared pointer to the pipeline context std::shared_ptr m_pipelineContext; }; using SinkContractPtr = std::unique_ptr; class Sink; using SinkPtr = std::shared_ptr; + + /// @brief The factory callback or lambda to create this sink. Used for plugins. using SinkFactoryFn = boost::function; + /// @brief Abstract Sink class AGENT_LIB_API Sink : public std::enable_shared_from_this { public: @@ -88,15 +125,30 @@ namespace mtconnect { {} virtual ~Sink() = default; + /// @brief The shared_from_this pointer for this object + /// @return shared pointer SinkPtr getptr() const { return const_cast(this)->shared_from_this(); } + /// @brief Start the sink virtual void start() = 0; + /// @brief stop the sink virtual void stop() = 0; + /// @brief Receive an observation + /// @param observation shared pointer to the observation + /// @return `true` if the publishing was successful virtual bool publish(observation::ObservationPtr &observation) = 0; + /// @brief Receive an asset + /// @param asset shared point to the asset + /// @return `true` if successful virtual bool publish(asset::AssetPtr asset) = 0; + /// @brief Receive a device + /// @param device shared pointer to the device + /// @return `true` if successful virtual bool publish(device_model::DevicePtr device) { return false; } + /// @brief Get the name of the Sink. Sinks should have unique names. + /// @return the name const auto &getName() const { return m_name; } protected: @@ -104,18 +156,35 @@ namespace mtconnect { std::string m_name; }; + /// @brief Factory to create sinks class AGENT_LIB_API SinkFactory { public: + /// @brief Register and associate the name with the Sink factory functon + /// @param name the name + /// @param function the factory void registerFactory(const std::string &name, SinkFactoryFn function) { m_factories.insert_or_assign(name, function); } + /// @brief Clear all factories void clear() { m_factories.clear(); } + /// @brief Check if a sink factory exists + /// @param name the name of the factory + /// @return `true` if the factory exits. bool hasFactory(const std::string &name) { return m_factories.count(name) > 0; } + /// @brief Create a sink for a given name + /// @param factoryName The name of the factory + /// @param sinkName The sink to be created + /// @param io a reference to the boost::asio io_context + /// @param contract The SinkContract to give to the Sink + /// @param options Configuration options for the sink + /// @param block Additional configuration options for the Sink as a boost property tree. + /// These options need to be interpreted by the sink + /// @return A shared pointer to the sink. SinkPtr make(const std::string &factoryName, const std::string &sinkName, boost::asio::io_context &io, SinkContractPtr &&contract, const ConfigOptions &options, const boost::property_tree::ptree &block); diff --git a/src/mtconnect/source/adapter/adapter.hpp b/src/mtconnect/source/adapter/adapter.hpp index d4cbe925..1989579a 100644 --- a/src/mtconnect/source/adapter/adapter.hpp +++ b/src/mtconnect/source/adapter/adapter.hpp @@ -21,22 +21,42 @@ #include "mtconnect/source/adapter/adapter_pipeline.hpp" #include "mtconnect/source/source.hpp" +/// @brief MTConnect Source Adapter namespace namespace mtconnect::source::adapter { + /// @brief Abstract adapter class AGENT_LIB_API Adapter : public Source { public: + /// @brief Create an adapter + /// @param name adapter name + /// @param io boost asio io context + /// @param options adapter options Adapter(const std::string &name, boost::asio::io_context &io, const ConfigOptions &options) : Source(name, io), m_options(options) {} virtual ~Adapter() {} + /// @name Agent Device methods + ///@{ + + /// @brief Get the host name + /// @return the host virtual const std::string &getHost() const = 0; + /// @brief Get the adapter's identity + /// @return the identity const std::string &getIdentity() const override { return m_identity; } + /// @brief Get the port + /// @return the port virtual unsigned int getPort() const = 0; + /// @brief Get the configuration options + /// @return configuration options virtual const ConfigOptions &getOptions() const { return m_options; } + /// @brief set the adapter handler + /// @param h the handler (takes ownership) void setHandler(std::unique_ptr &h) { m_handler = std::move(h); } - + ///@} + protected: std::string m_identity; std::unique_ptr m_handler; diff --git a/src/mtconnect/source/adapter/adapter_pipeline.hpp b/src/mtconnect/source/adapter/adapter_pipeline.hpp index c342e795..d6040878 100644 --- a/src/mtconnect/source/adapter/adapter_pipeline.hpp +++ b/src/mtconnect/source/adapter/adapter_pipeline.hpp @@ -22,6 +22,7 @@ #include "mtconnect/pipeline/transform.hpp" namespace mtconnect::source::adapter { + /// @brief Handler functions for handling data and connection status struct Handler { using ProcessData = std::function; @@ -29,25 +30,42 @@ namespace mtconnect::source::adapter { const std::string &source)>; using Connect = std::function; + /// @brief Process Data Messages ProcessData m_processData; + /// @brief Process an adapter command ProcessData m_command; + /// @brief Process a message with a topic ProcessMessage m_processMessage; + /// @brief method to call when connecting Connect m_connecting; + /// @brief method to call when connected Connect m_connected; + /// @brief method to call when disconnected Connect m_disconnected; }; + /// @brief The adapter pipeline with common pipeline construction methods. This class + /// is subclassed for particular adapters class AGENT_LIB_API AdapterPipeline : public pipeline::Pipeline { public: + /// @brief Create and adapter pipeline + /// @param context the pipeline context + /// @param st boost asio strand AdapterPipeline(pipeline::PipelineContextPtr context, boost::asio::io_context::strand &st) : Pipeline(context, st) {} + /// @brief build the pipeline + /// @param options the configuration options void build(const ConfigOptions &options) override; + /// @brief Create a handler + /// @return the handler handing over ownership virtual std::unique_ptr makeHandler(); + /// @brief get the associated device + /// @return the device const auto &getDevice() const { return m_device; } protected: diff --git a/src/mtconnect/source/adapter/agent_adapter/agent_adapter.hpp b/src/mtconnect/source/adapter/agent_adapter/agent_adapter.hpp index 7de6887d..75a37179 100644 --- a/src/mtconnect/source/adapter/agent_adapter/agent_adapter.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/agent_adapter.hpp @@ -28,13 +28,22 @@ namespace boost::asio::ssl { class context; } +/// @brief @brief the agent adapter namespace namespace mtconnect::source::adapter::agent_adapter { using namespace mtconnect; using namespace source::adapter; + /// @brief The Agent adapter pipeline class AGENT_LIB_API AgentAdapterPipeline : public AdapterPipeline { public: + /// @brief Create an adapter pipeline + /// + /// Feedback is used to determine if recovery is possible and when to start a + /// new http streaming session with the upstream agent + /// @param context the pipeline context + /// @param st strand to run in + /// @param feedback feedback from the pipeline to the adapter when an error occurs AgentAdapterPipeline(pipeline::PipelineContextPtr context, boost::asio::io_context::strand &st, pipeline::XmlTransformFeedback &feedback) : AdapterPipeline(context, st), m_feedback(feedback) @@ -45,12 +54,20 @@ namespace mtconnect::source::adapter::agent_adapter { pipeline::XmlTransformFeedback &m_feedback; }; + /// @brief An adapter to connnect to another Agent and replicate data class AGENT_LIB_API AgentAdapter : public Adapter { public: + /// @brief Create an agent adapter + /// @param io boost asio io context + /// @param context pipeline context + /// @param options configation options + /// @param block additional configuration options AgentAdapter(boost::asio::io_context &io, pipeline::PipelineContextPtr context, const ConfigOptions &options, const boost::property_tree::ptree &block); + /// @brief Register the agent adapter with the factory for `http` and `https` + /// @param factory source factory static void registerFactory(SourceFactory &factory) { auto cb = [](const std::string &name, boost::asio::io_context &io, @@ -63,17 +80,27 @@ namespace mtconnect::source::adapter::agent_adapter { factory.registerFactory("https", cb); } + /// @name Agent Device methods + ///@{ const std::string &getHost() const override { return m_host; } - unsigned int getPort() const override { return 0; } + ///@} + + /// @brief get a reference to the transform feedback + /// @return reference to the transform feedback auto &getFeedback() { return m_feedback; } ~AgentAdapter() override; + /// @name Source interface + ///@{ bool start() override; void stop() override; pipeline::Pipeline *getPipeline() override { return &m_pipeline; } + ///@} + /// @brief get a shared pointer to the source + /// @return shared pointer to this auto getptr() const { return std::dynamic_pointer_cast(Source::getptr()); } protected: diff --git a/src/mtconnect/source/adapter/agent_adapter/http_session.hpp b/src/mtconnect/source/adapter/agent_adapter/http_session.hpp index a8b1eead..1003a2cd 100644 --- a/src/mtconnect/source/adapter/agent_adapter/http_session.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/http_session.hpp @@ -23,12 +23,16 @@ namespace mtconnect::source::adapter::agent_adapter { - // HTTP Session + /// @brief HTTP Agent Adapter Session class HttpSession : public SessionImpl { public: + /// @brief The superclass is using a Derived class pattern using super = SessionImpl; + /// @brief Create a session to connect to the remote agent + /// @param ioc the asio strand to run in + /// @param url URL to connect to HttpSession(boost::asio::io_context::strand &ioc, const Url &url) : super(ioc, url), m_stream(ioc.context()) {} @@ -39,15 +43,26 @@ namespace mtconnect::source::adapter::agent_adapter { beast::get_lowest_layer(m_stream).close(); } + /// @brief Get a shared pointer to this + /// @return shared pointer to this shared_ptr getptr() { return static_pointer_cast(shared_from_this()); } + /// @brief Get the boost asio tcp stream + /// @return reference to the stream auto &stream() { return m_stream; } + /// @brief Get the lowest protocol layer to the tcp stream + /// @return lowest protocol layer auto &lowestLayer() { return beast::get_lowest_layer(m_stream); } + /// @brief Get an immutable lowest protocol layer to the tcp stream + /// @return const lowest protocol layer const auto &lowestLayer() const { return beast::get_lowest_layer(m_stream); } + /// @brief method called asynchonously when the source connects to the agent + /// @param ec an error code + /// @param endpoint_type unused void onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) { if (ec) @@ -70,6 +85,7 @@ namespace mtconnect::source::adapter::agent_adapter { request(); } + /// @brief disconnnect from the agent void disconnect() { beast::error_code ec; diff --git a/src/mtconnect/source/adapter/agent_adapter/https_session.hpp b/src/mtconnect/source/adapter/agent_adapter/https_session.hpp index 43b31fe3..0b5b0ad3 100644 --- a/src/mtconnect/source/adapter/agent_adapter/https_session.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/https_session.hpp @@ -25,12 +25,17 @@ namespace mtconnect::source::adapter::agent_adapter { - // HTTPS Session + /// @brief HTTPS Agent Adapter Session class HttpsSession : public SessionImpl { public: + /// @brief The superclass is using a Derived class pattern using super = SessionImpl; + /// @brief Construct an HTTPS Session to securely connect to another agent + /// @param ex the strand to run in + /// @param url the url to connect to + /// @param ctx the TLS context explicit HttpsSession(boost::asio::io_context::strand &ex, const Url &url, ssl::context &ctx) : super(ex, url), m_stream(ex.context(), ctx) {} @@ -40,8 +45,14 @@ namespace mtconnect::source::adapter::agent_adapter { beast::get_lowest_layer(m_stream).close(); } + /// @brief Get the boost asio ssl tcp stream + /// @return reference to the stream auto &stream() { return m_stream; } + /// @brief Get the lowest protocol layer to the ssl tcp stream + /// @return lowest protocol layer auto &lowestLayer() { return beast::get_lowest_layer(m_stream); } + /// @brief Get an immutable lowest protocol layer to the ssl tcp stream + /// @return const lowest protocol layer const auto &lowestLayer() const { return beast::get_lowest_layer(m_stream); } shared_ptr getptr() @@ -49,7 +60,6 @@ namespace mtconnect::source::adapter::agent_adapter { return std::static_pointer_cast(shared_from_this()); } - // Start the asynchronous operation void connect() override { if (!SSL_set_tlsext_host_name(m_stream.native_handle(), m_url.getHost().c_str())) @@ -62,6 +72,9 @@ namespace mtconnect::source::adapter::agent_adapter { super::connect(); } + /// @brief Callback when the async connect completes + /// @param ec an error code + /// @param endpoint_type unused void onConnect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) { if (ec) @@ -88,6 +101,8 @@ namespace mtconnect::source::adapter::agent_adapter { beast::bind_front_handler(&HttpsSession::onHandshake, getptr()))); } + /// @brief Callback from the TLS handshake after the connect completes + /// @param ec an error code void onHandshake(beast::error_code ec) { if (ec) @@ -103,6 +118,9 @@ namespace mtconnect::source::adapter::agent_adapter { request(); } + /// @brief Do a graceful async shutdown of the connection to the server + /// + /// Times out if no response void disconnect() { // Set a timeout on the operation @@ -113,6 +131,8 @@ namespace mtconnect::source::adapter::agent_adapter { m_strand, beast::bind_front_handler(&HttpsSession::onShutdown, getptr()))); } + /// @brief Callback from the shutdown + /// @param ec complete the shutdown and close the tcp socket void onShutdown(beast::error_code ec) { if (ec == asio::error::eof) diff --git a/src/mtconnect/source/adapter/agent_adapter/session.hpp b/src/mtconnect/source/adapter/agent_adapter/session.hpp index 7872cf69..c572009e 100644 --- a/src/mtconnect/source/adapter/agent_adapter/session.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/session.hpp @@ -35,6 +35,7 @@ namespace mtconnect::pipeline { namespace mtconnect::source::adapter::agent_adapter { struct AgentHandler; + /// @brief Abstract interface for an HTTP or HTTPS session class Session : public std::enable_shared_from_this { public: @@ -42,30 +43,54 @@ namespace mtconnect::source::adapter::agent_adapter { using Failure = std::function; using UpdateAssets = std::function; + /// @brief An HTTP Request wrapper struct Request { - Request(const std::optional &device, const std::string &suffix, + /// @brief Create a request + /// @param device optional device this request is targeting + /// @param operation the REST operation + /// @param query The URL query parameters + /// @param stream `true` if HTTP x-multipart-replace streaming is desired + /// @param next Function to determine what to do on successful read + Request(const std::optional &device, const std::string &operation, const UrlQuery &query, bool stream, Next next) - : m_sourceDevice(device), m_suffix(suffix), m_query(query), m_stream(stream), m_next(next) + : m_sourceDevice(device), m_operation(operation), m_query(query), m_stream(stream), m_next(next) {} Request(const Request &request) = default; - std::optional m_sourceDevice; - std::string m_suffix; - UrlQuery m_query; - bool m_stream; - Next m_next; + std::optional m_sourceDevice; ///< optional source device + std::string m_operation; ///< The REST operation (probe, current, sample, asset) + UrlQuery m_query; ///< URL Query parameters + bool m_stream; ///< `true` if using HTTP long pull + Next m_next; ///< function to call on successful read - auto getTarget(const Url &url) { return url.getTarget(m_sourceDevice, m_suffix, m_query); } + /// @brief Given a url, get a formatted target for a given operation + /// @param url The base url + /// @return a string with a new URL path and query (for the GET) + auto getTarget(const Url &url) { return url.getTarget(m_sourceDevice, m_operation, m_query); } }; virtual ~Session() {} + + /// @name Session interface + ///@{ + + /// @brief Is the current connection open + /// @return `true` if it is open virtual bool isOpen() const = 0; + /// @brief Stop the connection virtual void stop() = 0; + /// @brief Method called with something fails + /// @param ec the error code + /// @param what descriptive message virtual void failed(std::error_code ec, const char *what) = 0; + /// @brief Make a request of the remote agent + /// @param request the request + /// @return `true` if successful virtual bool makeRequest(const Request &request) = 0; + ///@} Handler *m_handler = nullptr; std::string m_identity; diff --git a/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp b/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp index 5d727eba..919a4d81 100644 --- a/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/session_impl.hpp @@ -38,14 +38,20 @@ namespace mtconnect::source::adapter::agent_adapter { namespace ssl = boost::asio::ssl; using tcp = boost::asio::ip::tcp; - // Report a failure + /// @brief A session implementation that where the derived classes can support HTTP or HTTPS + /// @tparam Derived template class SessionImpl : public Session { + /// @brief A list of HTTP requests using RequestQueue = std::list; public: + /// @brief Cast this class as the derived class + /// @return reference to the derived class Derived &derived() { return static_cast(*this); } + /// @brief Immutably cast this class as its derived subclass + /// @return const reference to the derived class const Derived &derived() const { return static_cast(*this); } // Objects are constructed with a strand to @@ -56,8 +62,15 @@ namespace mtconnect::source::adapter::agent_adapter { virtual ~SessionImpl() { stop(); } + /// @brief see if the socket is connected + /// @return `true` if the socket is open bool isOpen() const override { return derived().lowestLayer().socket().is_open(); } + /// @brief Method called when a request fails + /// + /// Closes the socket and resets the request + /// @param ec error code to report + /// @param what the reason why the failure occurred void failed(std::error_code ec, const char *what) override { derived().lowestLayer().socket().close(); @@ -109,6 +122,8 @@ namespace mtconnect::source::adapter::agent_adapter { } } + /// @brief Process data from the remote agent + /// @param data the payload from the agent void processData(const std::string &data) { try @@ -138,7 +153,7 @@ namespace mtconnect::source::adapter::agent_adapter { } } - // Start the asynchronous operation + /// @brief Connect to the remote agent virtual void connect() { // If the address is an IP address, we do not need to resolve. @@ -170,6 +185,9 @@ namespace mtconnect::source::adapter::agent_adapter { } } + /// @brief Callback when the host name needs to be resolved + /// @param ec error code if resultion fails + /// @param results the resolution results void onResolve(beast::error_code ec, tcp::resolver::results_type results) { if (ec) @@ -201,6 +219,7 @@ namespace mtconnect::source::adapter::agent_adapter { derived().getptr()))); } + /// @brief Write a request to the remote agent void request() { if (!m_request) @@ -226,6 +245,9 @@ namespace mtconnect::source::adapter::agent_adapter { beast::bind_front_handler(&SessionImpl::onWrite, derived().getptr())); } + /// @brief Callback on successful write to the agent + /// @param ec error code if something failed + /// @param bytes_transferred number of bytes transferred (unused) void onWrite(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); @@ -252,6 +274,9 @@ namespace mtconnect::source::adapter::agent_adapter { beast::bind_front_handler(&Derived::onHeader, derived().getptr()))); } + /// @brief Callback after write to process the message header + /// @param ec error code if something failed + /// @param bytes_transferred number of bytes transferred void onHeader(beast::error_code ec, std::size_t bytes_transferred) { if (ec) @@ -299,6 +324,9 @@ namespace mtconnect::source::adapter::agent_adapter { } } + /// @brief Callback after header processing to read the body of the response + /// @param ec error code if something failed + /// @param bytes_transferred number of bytes transferred (unused) void onRead(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); @@ -343,8 +371,11 @@ namespace mtconnect::source::adapter::agent_adapter { } } - // Streaming related methods + /// @name Streaming related methods + ///@{ + /// @brief Find the x-multipart-replace MIME boundary + /// @return the boundary string inline string findBoundary() { auto f = m_headerParser->get().find(http::field::content_type); @@ -370,6 +401,9 @@ namespace mtconnect::source::adapter::agent_adapter { return ""; } + /// @brief Create a function to handle the chunk header + /// + /// Sets the chunk parser on chunk header void createChunkHeaderHandler() { m_chunkHeaderHandler = [this](std::uint64_t size, boost::string_view extensions, @@ -391,6 +425,8 @@ namespace mtconnect::source::adapter::agent_adapter { m_chunkParser->on_chunk_header(m_chunkHeaderHandler); } + /// @brief Parse the header and get the size and type + /// @return `true` if successful bool parseMimeHeader() { using namespace boost; @@ -459,6 +495,9 @@ namespace mtconnect::source::adapter::agent_adapter { return true; } + /// @brief Creates the function to handle chunk body + /// + /// Sets the chunk parse on chunk body. void createChunkBodyHandler() { m_chunkHandler = [this](std::uint64_t remain, boost::string_view body, @@ -508,6 +547,7 @@ namespace mtconnect::source::adapter::agent_adapter { m_chunkParser->on_chunk_body(m_chunkHandler); } + /// @brief Begins the async chunk reading if the boundry is found void onChunkedContent() { m_boundary = findBoundary(); diff --git a/src/mtconnect/source/adapter/agent_adapter/url_parser.cpp b/src/mtconnect/source/adapter/agent_adapter/url_parser.cpp index 675ea213..955a123d 100644 --- a/src/mtconnect/source/adapter/agent_adapter/url_parser.cpp +++ b/src/mtconnect/source/adapter/agent_adapter/url_parser.cpp @@ -23,17 +23,21 @@ namespace qi = boost::spirit::qi; +/// @brief User credentials struct for intermediate parsing struct UserCred { std::string m_username; std::string m_password; }; +/// @brief make the struct available to spirit BOOST_FUSION_ADAPT_STRUCT(UserCred, (std::string, m_username)(std::string, m_password)) +/// @brief make the struct available to spirit BOOST_FUSION_ADAPT_STRUCT(mtconnect::source::adapter::agent_adapter::UrlQueryPair, (std::string, first)(std::string, second)) +/// @brief make the struct available to spirit BOOST_FUSION_ADAPT_STRUCT( mtconnect::source::adapter::agent_adapter::Url, (std::string, m_protocol)(mtconnect::source::adapter::agent_adapter::Url::Host, @@ -43,6 +47,12 @@ BOOST_FUSION_ADAPT_STRUCT( namespace mtconnect::source::adapter::agent_adapter { + /// @brief Convert from four uns8s to an ipv4 address + /// @param n1 first octet + /// @param n2 second octet + /// @param n3 third octet + /// @param n4 fourth octet + /// @return a boost ipv4 address static boost::asio::ip::address_v4 from_four_number(unsigned char n1, unsigned char n2, unsigned char n3, unsigned char n4) { @@ -56,6 +66,9 @@ namespace mtconnect::source::adapter::agent_adapter { return boost::asio::ip::address_v4(bt); } + /// @brief convert a string to an ipv6 address + /// @param str the string (as a char vector) + /// @return a boost ipv6 address static boost::asio::ip::address_v6 from_v6_string(std::vector str) { return boost::asio::ip::address_v6::from_string(str.data()); @@ -64,6 +77,8 @@ namespace mtconnect::source::adapter::agent_adapter { BOOST_PHOENIX_ADAPT_FUNCTION(boost::asio::ip::address_v4, v4_from_4number, from_four_number, 4) BOOST_PHOENIX_ADAPT_FUNCTION(boost::asio::ip::address_v6, v6_from_string, from_v6_string, 1) + /// @brief The Uri parser spirit qi grammar + /// @tparam Iterator template struct UriGrammar : qi::grammar { diff --git a/src/mtconnect/source/adapter/agent_adapter/url_parser.hpp b/src/mtconnect/source/adapter/agent_adapter/url_parser.hpp index 7d0f58ba..daa14724 100644 --- a/src/mtconnect/source/adapter/agent_adapter/url_parser.hpp +++ b/src/mtconnect/source/adapter/agent_adapter/url_parser.hpp @@ -33,9 +33,13 @@ namespace mtconnect::source::adapter::agent_adapter { using UrlQueryPair = std::pair; + /// @brief A map of URL query parameters that can format as a string struct AGENT_LIB_API UrlQuery : public std::map { using std::map::map; + + /// @brief join the parameters as `=&=&...` + /// @return std::string join() const { std::stringstream ss; @@ -53,6 +57,8 @@ namespace mtconnect::source::adapter::agent_adapter { return ss.str(); } + /// @brief Merge twos sets over-writing existing pairs set with `query` and adding new pairs + /// @param query query to merge void merge(UrlQuery query) { for (const auto& kv : query) @@ -62,20 +68,23 @@ namespace mtconnect::source::adapter::agent_adapter { } }; + /// @brief URL struct to parse and format URLs struct AGENT_LIB_API Url { + /// @brief Variant for the Host that is either a host name or an ip address using Host = std::variant; - std::string m_protocol; // http/https + std::string m_protocol; ///< either `http` or `https` - Host m_host; - std::optional m_username; - std::optional m_password; - std::optional m_port; - std::string m_path = "/"; - UrlQuery m_query; - std::string m_fragment; + Host m_host; ///< the host component + std::optional m_username; ///< optional username + std::optional m_password; ///< optional password + std::optional m_port; ///< The optional port number (defaults to 5000) + std::string m_path = "/"; ///< The path component + UrlQuery m_query; ///< Query parameters + std::string m_fragment; ///< The component after a `#` + /// @brief Visitor to format the Host as a string struct HostVisitor { std::string operator()(std::string v) const { return v; } @@ -83,11 +92,15 @@ namespace mtconnect::source::adapter::agent_adapter { std::string operator()(boost::asio::ip::address v) const { return v.to_string(); } }; + /// @brief Get the host as a string + /// @return the host std::string getHost() const { return std::visit(HostVisitor(), m_host); } - + /// @brief Get the port as a string + /// @return the port std::string getService() const { return boost::lexical_cast(getPort()); } - // the path with query and without fragment + /// @brief Get the path and the query portion of the URL + /// @return the path and query std::string getTarget() const { if (m_query.size()) @@ -96,6 +109,11 @@ namespace mtconnect::source::adapter::agent_adapter { return m_path; } + /// @brief Format a target using the existing host and port to make a request + /// @param device an optional device + /// @param operation the operation (probe,sample,current, or asset) + /// @param query query parameters + /// @return A string with the target for a GET reuest std::string getTarget(const std::optional& device, const std::string& operation, const UrlQuery& query) const { @@ -129,6 +147,9 @@ namespace mtconnect::source::adapter::agent_adapter { return 0; } + /// @brief Format the URL as text + /// @param device optional device to add to the URL + /// @return formatted URL std::string getUrlText(const std::optional& device) { std::stringstream url; @@ -138,6 +159,8 @@ namespace mtconnect::source::adapter::agent_adapter { return url.str(); } + /// @brief parse a string to a Url + /// @return parsed URL static Url parse(const std::string_view& url); }; diff --git a/src/mtconnect/source/adapter/shdr/connector.hpp b/src/mtconnect/source/adapter/shdr/connector.hpp index 183a48d4..7ed948bf 100644 --- a/src/mtconnect/source/adapter/shdr/connector.hpp +++ b/src/mtconnect/source/adapter/shdr/connector.hpp @@ -30,22 +30,30 @@ #define HEARTBEAT_FREQ 60000 namespace mtconnect::source::adapter::shdr { + /// @brief Connection to an adapter socket class AGENT_LIB_API Connector { public: - // Instantiate the server by assigning it a server and port/ + /// @brief Instantiate the server by assigning it a server and port + /// @param strand boost asio strand + /// @param server server to connect to + /// @param port port to connect to + /// @param legacyTimout connection timeout (defaulted to 5 minutes) + /// @param reconnectInterval time between reconnection attempts (defaults to 10 seconds) Connector(boost::asio::io_context::strand &strand, std::string server, unsigned int port, std::chrono::seconds legacyTimout = std::chrono::seconds {600}, std::chrono::seconds reconnectInterval = std::chrono::seconds {10}); - // Virtual desctructor virtual ~Connector(); - // Blocking call to connect to the server/port - // Put data from the socket in the string buffer - // + /// @brief Blocking call to connect to the server/port + /// Put data from the socket in the string buffer virtual bool start(); + /// @brief resolve the adapter host address + /// @return `bool` if it can b resolved virtual bool resolve(); + /// @brief connect to the adapter + /// @return `true` if it can connect virtual bool connect(); // Abstract method to handle what to do with each line of data from Socket diff --git a/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp b/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp index 4c839b10..169d61fe 100644 --- a/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp +++ b/src/mtconnect/source/adapter/shdr/shdr_adapter.hpp @@ -39,15 +39,23 @@ namespace mtconnect { class Device; } + /// @brief namespace for shdr adapter sources namespace source::adapter::shdr { + /// @brief The SHDR adapter client source class AGENT_LIB_API ShdrAdapter : public adapter::Adapter, public Connector { public: - // Associate adapter with a device & connect to the server & port + /// @brief Associate adapter with a device & connect to the server & port + /// @param[in] io boost asio io conext + /// @param[in] pipelineContext pipeline context + /// @param[in] options configuration options + /// @param[in] block additional configuration options not in options ShdrAdapter(boost::asio::io_context &io, pipeline::PipelineContextPtr pipelineContext, const ConfigOptions &options, const boost::property_tree::ptree &block); ShdrAdapter(const ShdrAdapter &) = delete; + /// @brief Factory registration method associate this source with `shdr` + /// @param[in] factory the source factory static void registerFactory(SourceFactory &factory) { factory.registerFactory( @@ -63,9 +71,12 @@ namespace mtconnect { // Virtual destructor ~ShdrAdapter() override { stop(); } + /// @brief The termination text when collecting multi-line data + /// @return the termination text auto &getTerminator() const { return m_terminator; } - // Inherited method to incoming data from the server + /// @name Source interface + ///@{ void processData(const std::string &data) override; void protocolCommand(const std::string &data) override; @@ -85,13 +96,6 @@ namespace mtconnect { if (m_handler && m_handler->m_connected) m_handler->m_connected(getIdentity()); } - - // Agent Device methods - const std::string &getHost() const override { return m_server; } - unsigned int getPort() const override { return m_port; } - pipeline::Pipeline *getPipeline() override { return &m_pipeline; } - - // Start and Stop void stop() override; bool start() override { @@ -103,7 +107,17 @@ namespace mtconnect { else return false; } + ///@} + + /// @name Agent Device methods + ///@{ + const std::string &getHost() const override { return m_server; } + unsigned int getPort() const override { return m_port; } + pipeline::Pipeline *getPipeline() override { return &m_pipeline; } + ///@} + /// @brief Change the options for the adapter + /// @param[in] options the set of options void setOptions(const ConfigOptions &options) { for (auto &o : options) diff --git a/src/mtconnect/source/adapter/shdr/shdr_pipeline.hpp b/src/mtconnect/source/adapter/shdr/shdr_pipeline.hpp index 29177e78..5962834f 100644 --- a/src/mtconnect/source/adapter/shdr/shdr_pipeline.hpp +++ b/src/mtconnect/source/adapter/shdr/shdr_pipeline.hpp @@ -21,9 +21,13 @@ #include "mtconnect/source/adapter/adapter_pipeline.hpp" namespace mtconnect::source::adapter::shdr { + /// @brief Pipeline for the SHDR adapter class AGENT_LIB_API ShdrPipeline : public AdapterPipeline { public: + /// @brief Create a pipeline for the SHDR Adapter + /// @param context the pipeline context + /// @param st boost asio strand for this source ShdrPipeline(pipeline::PipelineContextPtr context, boost::asio::io_context::strand &st) : AdapterPipeline(context, st) {} diff --git a/src/mtconnect/source/error_code.hpp b/src/mtconnect/source/error_code.hpp index e3fd8b90..0d341702 100644 --- a/src/mtconnect/source/error_code.hpp +++ b/src/mtconnect/source/error_code.hpp @@ -24,15 +24,16 @@ #include "mtconnect/config.hpp" namespace mtconnect::source { + /// @brief Reasons why the source failed enum class ErrorCode { OK = 0, - ADAPTER_FAILED, - STREAM_CLOSED, - INSTANCE_ID_CHANGED, - RESTART_STREAM, - RETRY_REQUEST, - MULTIPART_STREAM_FAILED + ADAPTER_FAILED, ///< The adapter failed + STREAM_CLOSED, ///< The stream closed + INSTANCE_ID_CHANGED, ///< The source instance id changed + RESTART_STREAM, ///< The stream needed to be restarted + RETRY_REQUEST, ///< The request needs to be retried + MULTIPART_STREAM_FAILED ///< The multipart stream failed }; } @@ -47,6 +48,7 @@ namespace std { } // namespace std namespace mtconnect::source { + /// @brief Error categories for error reporting using std:error_code and std::error_condition struct ErrorCategory : std::error_category { const char *name() const noexcept override { return "MTConnect::Error"; } diff --git a/src/mtconnect/source/loopback_source.hpp b/src/mtconnect/source/loopback_source.hpp index 5d9b85e0..cd396e04 100644 --- a/src/mtconnect/source/loopback_source.hpp +++ b/src/mtconnect/source/loopback_source.hpp @@ -26,21 +26,33 @@ #include "mtconnect/utilities.hpp" namespace mtconnect::source { + /// @brief A pipeline for a loopback source class AGENT_LIB_API LoopbackPipeline : public pipeline::Pipeline { public: + /// @brief Create a loopback pipeline + /// @param[in] context pipeline context + /// @param[in] st boost asio strand LoopbackPipeline(pipeline::PipelineContextPtr context, boost::asio::io_context::strand &st) : pipeline::Pipeline(context, st) {} + /// @brief build the pipeline + /// @param options configuration options void build(const ConfigOptions &options) override; protected: ConfigOptions m_options; }; + /// @brief Loopback source for sending entities back to the agent class AGENT_LIB_API LoopbackSource : public Source { public: + /// @brief Create a loopback source + /// @param name the name of the source + /// @param io boost asio strand + /// @param pipelineContext pipeline context + /// @param options loopback source options LoopbackSource(const std::string &name, boost::asio::io_context::strand &io, pipeline::PipelineContextPtr pipelineContext, const ConfigOptions &options) : Source(name, io), m_pipeline(pipelineContext, Source::m_strand) @@ -48,6 +60,8 @@ namespace mtconnect::source { m_pipeline.build(options); } + /// @brief this is a loopback source + /// @return always `true` bool isLoopback() override { return true; } bool start() override @@ -58,6 +72,9 @@ namespace mtconnect::source { void stop() override { m_pipeline.clear(); } pipeline::Pipeline *getPipeline() override { return &m_pipeline; } + /// @brief send an observation running it through the pipeline + /// @param observation the observation + /// @return the sequence number SequenceNumber_t receive(observation::ObservationPtr observation) { auto res = m_pipeline.run(observation); @@ -70,17 +87,43 @@ namespace mtconnect::source { return 0; } } + /// @brief create and send an observation through the pipeline + /// @param dataItem the observation's data item + /// @param props observation properties + /// @param timestamp optional observation timestamp + /// @return the sequence number SequenceNumber_t receive(DataItemPtr dataItem, entity::Properties props, std::optional timestamp = std::nullopt); + /// @brief create and send an observation through the pipeline + /// @param dataItem the observation's data item + /// @param value simple string value + /// @param timestamp optional observation timestamp + /// @return the sequence number SequenceNumber_t receive(DataItemPtr dataItem, const std::string &value, std::optional timestamp = std::nullopt); + /// @brief create and send an observation with shdr through the pipeline + /// @param shdr shdr pipe deliminated text + /// @return the sequence number SequenceNumber_t receive(const std::string &shdr); + /// @brief send an asset through the pipeline + /// @param asset the asset void receive(asset::AssetPtr asset) { m_pipeline.run(asset); } + /// @brief create and send an asset through the pipeline + /// @param device the device associated with the asset + /// @param document the asset document + /// @param id optional asset id + /// @param type optional asset type + /// @param time optional asset timestamp + /// @param[out] errors errors if any occurred + /// @return shared pointer to the asset asset::AssetPtr receiveAsset(DevicePtr device, const std::string &document, const std::optional &id, const std::optional &type, const std::optional &time, entity::ErrorList &errors); + /// @brief set a remove asset command through the pipeline + /// @param device optional device + /// @param id the asset id void removeAsset(const std::optional device, const std::string &id); protected: diff --git a/src/mtconnect/source/source.cpp b/src/mtconnect/source/source.cpp index f529d708..5cc0322c 100644 --- a/src/mtconnect/source/source.cpp +++ b/src/mtconnect/source/source.cpp @@ -20,8 +20,8 @@ #include "mtconnect/logging.hpp" namespace mtconnect::source { - source::SourcePtr SourceFactory::make(const std::string &factoryName, const std::string &sinkName, - boost::asio::io_context &io, + source::SourcePtr SourceFactory::make(const std::string &factoryName, + const std::string &sourceName, boost::asio::io_context &io, std::shared_ptr context, const ConfigOptions &options, const boost::property_tree::ptree &block) @@ -29,7 +29,7 @@ namespace mtconnect::source { auto factory = m_factories.find(factoryName); if (factory != m_factories.end()) { - return factory->second(sinkName, io, context, options, block); + return factory->second(sourceName, io, context, options, block); } else { diff --git a/src/mtconnect/source/source.hpp b/src/mtconnect/source/source.hpp index b98ce27b..38c6a71c 100644 --- a/src/mtconnect/source/source.hpp +++ b/src/mtconnect/source/source.hpp @@ -29,6 +29,7 @@ namespace mtconnect { class PipelineContext; } // namespace pipeline + /// @brief The data source namespace namespace source { class Source; using SourcePtr = std::shared_ptr; @@ -37,27 +38,53 @@ namespace mtconnect { std::shared_ptr pipelineContext, const ConfigOptions &options, const boost::property_tree::ptree &block)>; + /// @brief Abstract agent data source class AGENT_LIB_API Source : public std::enable_shared_from_this { public: + /// @brief Create a source with an io context + /// @param io boost asio io context Source(boost::asio::io_context &io) : m_strand(io) {} + /// @brief Create a source with a strand + /// @param io boost asio strand Source(boost::asio::io_context::strand &io) : m_strand(io) {} + /// @brief Create a named source with an io context + /// @param name source name + /// @param io boost asio io context Source(const std::string &name, boost::asio::io_context &io) : m_name(name), m_strand(io) {} + /// @brief Create a named source with a strand + /// @param name source name + /// @param io boost asio strand Source(const std::string &name, boost::asio::io_context::strand &io) : m_name(name), m_strand(io) {} virtual ~Source() {} + /// @brief get a shared pointer to the source + /// @return shared pointer to this SourcePtr getptr() const { return const_cast(this)->shared_from_this(); } + /// @brief start the source + /// @return `true` if it succeeded virtual bool start() = 0; + /// @brief stop the source virtual void stop() = 0; + /// @brief check if this is a loopback source + /// @return `true` if it is a loopback source virtual bool isLoopback() { return false; } + /// @brief get the identity of the source + /// @return the identity virtual const std::string &getIdentity() const { return m_name; } + /// @brief get the pipeline associated with the source + /// @return pointer to the pipeline virtual pipeline::Pipeline *getPipeline() = 0; + /// @brief get the name of the source + /// @return the name const auto &getName() const { return m_name; } + /// @brief get the source's strand + /// @return the asio strand boost::asio::io_context::strand &getStrand(); protected: @@ -65,21 +92,36 @@ namespace mtconnect { boost::asio::io_context::strand m_strand; }; + /// @brief A factory for creating the source class AGENT_LIB_API SourceFactory { public: - SourcePtr make(const std::string &factoryName, const std::string &sinkName, + /// @brief make a source using this factory + /// @param factoryName the name of the the factory + /// @param sourceName a the name of the source + /// @param io the boost asio io context + /// @param context pipeline context + /// @param options configuration options + /// @param block additional options + /// @return shared pointer to the source + SourcePtr make(const std::string &factoryName, const std::string &sourceName, boost::asio::io_context &io, std::shared_ptr context, const ConfigOptions &options, const boost::property_tree::ptree &block); + /// @brief Register the factory with the factory name + /// @param name the name of the factory + /// @param function factory function to create a source void registerFactory(const std::string &name, SourceFactoryFn function) { m_factories.insert_or_assign(name, function); } + /// @brief clear the factories void clear() { m_factories.clear(); } - + /// @brief check if a factory exists + /// @param name the name of the factory + /// @return `true` if the factory is registered bool hasFactory(const std::string &name) { return m_factories.count(name) > 0; } private: diff --git a/src/mtconnect/utilities.hpp b/src/mtconnect/utilities.hpp index d213785a..4db028d0 100644 --- a/src/mtconnect/utilities.hpp +++ b/src/mtconnect/utilities.hpp @@ -15,6 +15,9 @@ // limitations under the License. // +/// @file utilities.hpp +/// @brief Common utility functions + #pragma once #include @@ -40,20 +43,25 @@ const unsigned int DEFAULT_SLIDING_BUFFER_SIZE = 131072; const unsigned int DEFAULT_SLIDING_BUFFER_EXP = 17; const unsigned int DEFAULT_MAX_ASSETS = 1024; +/// @brief MTConnect namespace +/// +/// Top level mtconnect namespace namespace mtconnect { // Message for when enumerations do not exist in an array/enumeration const int ENUM_MISS = -1; - // Time format + /// @brief Time formats enum TimeFormat { - HUM_READ, - GMT, - GMT_UV_SEC, - LOCAL + HUM_READ, ///< Human readable + GMT, ///< GMT or UTC with second resolution + GMT_UV_SEC, ///< GMT with microsecond resolution + LOCAL ///< Time using local time zone }; - //####### METHODS ####### + /// @brief Converts string to floating point numberss + /// @param[in] text the number + /// @return the converted value or 0.0 if incorrect. inline double stringToFloat(const std::string &text) { double value = 0.0; @@ -72,6 +80,9 @@ namespace mtconnect { return value; } + /// @brief Converts string to integer + /// @param[in] text the number + /// @return the converted value or 0 if incorrect. inline int stringToInt(const std::string &text, int outOfRangeDefault) { int value = 0; @@ -90,7 +101,9 @@ namespace mtconnect { return value; } - // Convert a float to string + /// @brief converts a double to a string + /// @param[in] value the double + /// @return the string representation of the double (10 places max) inline std::string format(double value) { std::stringstream s; @@ -99,14 +112,23 @@ namespace mtconnect { return s.str(); } + /// @brief inline formattor support for doubles class format_double_stream { protected: double val; public: + /// @brief create a formatter + /// @param[in] v the value format_double_stream(double v) { val = v; } + /// @brief writes a double to an output stream with up to 10 digits of precision + /// @tparam _CharT from std::basic_ostream + /// @tparam _Traits from std::basic_ostream + /// @param[in,out] os output stream + /// @param[in] fmter reference to this formatter + /// @return reference to the output stream template inline friend std::basic_ostream<_CharT, _Traits> &operator<<( std::basic_ostream<_CharT, _Traits> &os, const format_double_stream &fmter) @@ -117,9 +139,14 @@ namespace mtconnect { } }; + /// @brief create a `format_doulble_stream` + /// @param[in] v the value + /// @return the format_double_stream inline format_double_stream formatted(double v) { return format_double_stream(v); } - // Convert a string to the same string with all upper case letters + /// @brief Convert text to upper case + /// @param[in,out] text text + /// @return upper-case of text as string inline std::string toUpperCase(std::string &text) { std::transform(text.begin(), text.end(), text.begin(), @@ -128,7 +155,9 @@ namespace mtconnect { return text; } - // Check if each char in a string is a positive integer + /// @brief Simple check if a number as a string is negative + /// @param s the numbeer + /// @return `true` if positive inline bool isNonNegativeInteger(const std::string &s) { for (const char c : s) @@ -140,6 +169,9 @@ namespace mtconnect { return true; } + /// @brief Checks if a string is a valid integer + /// @param s the string + /// @return `true` if is `[+-]\d+` inline bool isInteger(const std::string &s) { auto iter = s.cbegin(); @@ -155,9 +187,15 @@ namespace mtconnect { return true; } + /// @brief Gets the local time + /// @param[in] time the time + /// @param[out] buf struct tm AGENT_LIB_API void mt_localtime(const time_t *time, struct tm *buf); - // Get a specified time formatted + /// @brief Formats the timePoint as string given the format + /// @param[in] timePoint the time + /// @param[in] format the format + /// @return the time as a string inline std::string getCurrentTime(std::chrono::time_point timePoint, TimeFormat format) { @@ -185,12 +223,20 @@ namespace mtconnect { return ""; } - // Get the current time formatted + /// @brief get the current time in the given format + /// + /// cover method for `getCurrentTime()` with `system_clock::now()` + /// + /// @param[in] format the format for the time + /// @return the time as a text inline std::string getCurrentTime(TimeFormat format) { return getCurrentTime(std::chrono::system_clock::now(), format); } + /// @brief Get the current time as a unsigned uns64 since epoch + /// @tparam timePeriod the resolution type of time + /// @return the time as an uns64 template inline uint64_t getCurrentTimeIn() { @@ -199,14 +245,21 @@ namespace mtconnect { .count(); } - // time_t to the ms + /// @brief Current time in microseconds since epoch + /// @return the time as uns64 in microsecnods inline uint64_t getCurrentTimeInMicros() { return getCurrentTimeIn(); } + /// @brief Current time in seconds since epoch + /// @return the time as uns64 in seconds inline uint64_t getCurrentTimeInSec() { return getCurrentTimeIn(); } + /// @brief Parse the given time + /// @param aTime the time in text + /// @return uns64 in microseconds since epoch AGENT_LIB_API uint64_t parseTimeMicro(const std::string &aTime); - // Replace illegal XML characters with the correct corresponding characters + /// @brief escaped reserved XML characters from text + /// @param data text with reserved characters escaped inline void replaceIllegalCharacters(std::string &data) { for (auto i = 0u; i < data.length(); i++) @@ -230,9 +283,16 @@ namespace mtconnect { } } + /// @brief add namespace prefixes to each element of the XPath + /// @param[in] aPath the path to modify + /// @param[in] aPrefix the prefix to add + /// @return the modified path prefixed AGENT_LIB_API std::string addNamespace(const std::string aPath, const std::string aPrefix); - // Ends with + /// @brief determines of a string ends with an ending + /// @param[in] value the string to check + /// @param[in] ending the ending to verify + /// @return `true` if the string ends with ending inline bool ends_with(const std::string &value, const std::string_view &ending) { if (ending.size() > value.size()) @@ -240,26 +300,37 @@ namespace mtconnect { return std::equal(ending.rbegin(), ending.rend(), value.rbegin()); } + /// @brief removes white space at the beginning of a string + /// @param[in,out] s the string + /// @return string with spaces removed inline std::string ltrim(std::string s) { boost::algorithm::trim_left(s); return s; } - // trim from end (in place) + /// @brief removes whitespace from the end of the string + /// @param[in,out] s the string + /// @return string with spaces removed static inline std::string rtrim(std::string s) { boost::algorithm::trim_right(s); return s; } - // trim from both ends (in place) + /// @brief removes spaces from the beginning and end of a string + /// @param[in] s the string + /// @return string with spaces removed inline std::string trim(std::string s) { boost::algorithm::trim(s); return s; } + /// @brief determines of a string starts with a beginning + /// @param[in] value the string to check + /// @param[in] beginning the beginning to verify + /// @return `true` if the string begins with beginning inline bool starts_with(const std::string &value, const std::string_view &beginning) { if (beginning.size() > value.size()) @@ -267,6 +338,10 @@ namespace mtconnect { return std::equal(beginning.begin(), beginning.end(), value.begin()); } + /// @brief Case insensitive equals + /// @param a first string + /// @param b second string + /// @return `true` if equal inline bool iequals(const std::string &a, const std::string_view &b) { if (a.size() != b.size()) @@ -279,6 +354,8 @@ namespace mtconnect { using Attributes = std::map; + /// @brief overloaded pattern for variant visitors using list of lambdas + /// @tparam ...Ts list of lambda classes template struct overloaded : Ts... { @@ -287,6 +364,8 @@ namespace mtconnect { template overloaded(Ts...) -> overloaded; + /// @brief Reverse an iterable + /// @tparam T The iterable type template class reverse { @@ -299,7 +378,9 @@ namespace mtconnect { auto end() const { return std::rend(m_iterable); } }; + /// @brief observation sequence type using SequenceNumber_t = uint64_t; + /// @brief set of data item ids for filtering using FilterSet = std::set; using FilterSetOpt = std::optional; using Milliseconds = std::chrono::milliseconds; @@ -307,9 +388,18 @@ namespace mtconnect { using Seconds = std::chrono::seconds; using Timestamp = std::chrono::time_point; using StringList = std::list; + + /// @brief Variant for configuration options using ConfigOption = std::variant; + /// @brief A map of name to option value using ConfigOptions = std::map; + + /// @brief Get an option if available + /// @tparam T the option type + /// @param options the set of options + /// @param name the name to get + /// @return the value of the option otherwise std::nullopt template inline const std::optional GetOption(const ConfigOptions &options, const std::string &name) { @@ -320,6 +410,10 @@ namespace mtconnect { return std::nullopt; } + /// @brief checks if a boolean option is set + /// @param options the set of options + /// @param name the name of the option + /// @return `true` if the option exists and has a bool type inline bool IsOptionSet(const ConfigOptions &options, const std::string &name) { auto v = options.find(name); @@ -329,12 +423,20 @@ namespace mtconnect { return false; } + /// @brief checks if there is an option + /// @param[in] options the set of options + /// @param[in] name the name of the option + /// @return `true` if the option exists inline bool HasOption(const ConfigOptions &options, const std::string &name) { auto v = options.find(name); return v != options.end(); } + /// @brief convert an option from a string to a typed option + /// @param[in] s the + /// @param[in] def template for the option + /// @return a typed option matching `def` inline auto ConvertOption(const std::string &s, const ConfigOption &def) { ConfigOption option; @@ -354,6 +456,17 @@ namespace mtconnect { return option; } + /// @brief convert from a string option to a size + /// + /// Recognizes the following suffixes: + /// - [Gg]: Gigabytes + /// - [Mm]: Megabytes + /// - [Kk]: Kilobytes + /// + /// @param[in] options A set of options + /// @param[in] name the name of the options + /// @param[in] size the default size (0) + /// @return the size honoring suffixes inline int64_t ConvertFileSize(const ConfigOptions &options, const std::string &name, int64_t size = 0) { @@ -399,6 +512,10 @@ namespace mtconnect { return size; } + /// @brief adds a property tree node to an option set + /// @param[in] tree the property tree coming from configuration parser + /// @param[in,out] options the options set + /// @param[in] entries a set of typed options to check inline void AddOptions(const boost::property_tree::ptree &tree, ConfigOptions &options, const ConfigOptions &entries) { @@ -414,6 +531,10 @@ namespace mtconnect { } } + /// @brief adds a property tree node to an option set with defaults + /// @param[in] tree the property tree coming from configuration parser + /// @param[in,out] options the option set + /// @param[in] entries the options with default values inline void AddDefaultedOptions(const boost::property_tree::ptree &tree, ConfigOptions &options, const ConfigOptions &entries) { @@ -431,6 +552,9 @@ namespace mtconnect { } } + /// @brief combine two option sets + /// @param[in,out] options existing set of options + /// @param[in] entries options to add or update inline void MergeOptions(ConfigOptions &options, const ConfigOptions &entries) { for (auto &e : entries) @@ -439,6 +563,10 @@ namespace mtconnect { } } + /// @brief get options from a property tree and create typed options + /// @param[in] tree the property tree coming from configuration parser + /// @param[in,out] options option set to modify + /// @param[in] entries a set of typed options to check inline void GetOptions(const boost::property_tree::ptree &tree, ConfigOptions &options, const ConfigOptions &entries) { @@ -453,6 +581,9 @@ namespace mtconnect { AddOptions(tree, options, entries); } + /// @brief Format a timestamp as a string in microseconds + /// @param[in] ts the timestamp + /// @return the time with microsecond resolution inline std::string format(const Timestamp &ts) { using namespace std; @@ -468,6 +599,12 @@ namespace mtconnect { return time; } + /// @brief Capitalize a word + /// + /// Has special treatment of acronyms like AC, DC, PH, etc. + /// + /// @param[in,out] start starting iterator + /// @param[in,out] end ending iterator inline void capitalize(std::string::iterator start, std::string::iterator end) { using namespace std; @@ -490,6 +627,14 @@ namespace mtconnect { } } + /// @brief creates an upper-camel-case string from words separated by an underscore (`_`) with + /// optional prefix + /// + /// Uses `capitalize()` method to capitalize words. + /// + /// @param[in] type the words to capitalize + /// @param[out] prefix the prefix of the string + /// @return a pascalized upper-camel-case string inline std::string pascalize(const std::string &type, std::optional &prefix) { using namespace std; @@ -526,6 +671,9 @@ namespace mtconnect { return camel; } + /// @brief parse a string timestamp to a `Timestamp` + /// @param timestamp[in] the timestamp as a string + /// @return converted `Timestamp` inline Timestamp parseTimestamp(const std::string ×tamp) { using namespace date; @@ -543,8 +691,11 @@ namespace mtconnect { return ts; } +/// @brief Creates a comparable schema version from a major and minor number #define SCHEMA_VERSION(major, minor) (major * 100 + minor) + /// @brief Get the default schema version of the agent as a string + /// @return the version inline std::string StrDefaultSchemaVersion() { return std::to_string(AGENT_VERSION_MAJOR) + "." + std::to_string(AGENT_VERSION_MINOR); @@ -555,6 +706,8 @@ namespace mtconnect { return SCHEMA_VERSION(AGENT_VERSION_MAJOR, AGENT_VERSION_MINOR); } + /// @brief convert a string version to a major and minor as two integers separated by a char. + /// @param s the version inline int32_t IntSchemaVersion(const std::string &s) { int major, minor; diff --git a/test/agent_adapter_test.cpp b/test/agent_adapter_test.cpp index 7ffdf82b..b193b5fa 100644 --- a/test/agent_adapter_test.cpp +++ b/test/agent_adapter_test.cpp @@ -159,7 +159,7 @@ class AgentAdapterTest : public testing::Test void addAdapter(ConfigOptions options = ConfigOptions {}) { m_agentTestHelper->addAdapter(options, "localhost", 7878, - m_agentTestHelper->m_agent->defaultDevice()->getName()); + m_agentTestHelper->m_agent->getDefaultDevice()->getName()); } public: diff --git a/test/agent_device_test.cpp b/test/agent_device_test.cpp index c0062f0b..84899f8a 100644 --- a/test/agent_device_test.cpp +++ b/test/agent_device_test.cpp @@ -152,6 +152,7 @@ TEST_F(AgentDeviceTest, DeviceAddedItemsInBuffer) { auto agent = m_agentTestHelper->getAgent(); auto device = agent->findDeviceByUUIDorName("000"); + auto &circ = agent->getCircularBuffer(); ASSERT_TRUE(device); auto uuid = *device->getUuid(); ASSERT_EQ("000", uuid); @@ -159,9 +160,9 @@ TEST_F(AgentDeviceTest, DeviceAddedItemsInBuffer) auto rest = m_agentTestHelper->getRestService(); - for (auto seq = rest->getSequence() - 1; !found && seq > 0ull; seq--) + for (auto seq = circ.getSequence() - 1; !found && seq > 0ull; seq--) { - auto event = rest->getFromBuffer(seq); + auto event = circ.getFromBuffer(seq); if (event->getDataItem()->getType() == "DEVICE_ADDED" && uuid == event->getValue()) { found = true; diff --git a/test/agent_test.cpp b/test/agent_test.cpp index 5b8371c4..f770e305 100644 --- a/test/agent_test.cpp +++ b/test/agent_test.cpp @@ -74,7 +74,7 @@ class AgentTest : public testing::Test void addAdapter(ConfigOptions options = ConfigOptions {}) { m_agentTestHelper->addAdapter(options, "localhost", 7878, - m_agentTestHelper->m_agent->defaultDevice()->getName()); + m_agentTestHelper->m_agent->getDefaultDevice()->getName()); } public: @@ -239,8 +239,8 @@ TEST_F(AgentTest, CurrentAt) addAdapter(); // Get the current position - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); char line[80] = {0}; // Add many events @@ -281,7 +281,7 @@ TEST_F(AgentTest, CurrentAt) // Check the first couple of items in the list for (int j = 0; j < 10; j++) { - auto i = rest->getSequence() - rest->getBufferSize() - seq + j; + auto i = circ.getSequence() - circ.getBufferSize() - seq + j; query["at"] = to_string(i + seq); ; PARSE_XML_RESPONSE_QUERY("/current", query); @@ -290,7 +290,7 @@ TEST_F(AgentTest, CurrentAt) // Test out of range... { - auto i = rest->getSequence() - rest->getBufferSize() - seq - 1; + auto i = circ.getSequence() - circ.getBufferSize() - seq - 1; sprintf(line, "'at' must be greater than %d", int32_t(i + seq)); query["at"] = to_string(i + seq); ; @@ -310,9 +310,10 @@ TEST_F(AgentTest, CurrentAt64) char line[80] = {0}; // Initialize the sliding buffer at a very large number. - auto rest = m_agentTestHelper->getRestService(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + uint64_t start = (((uint64_t)1) << 48) + 1317; - rest->setSequence(start); + circ.setSequence(start); // Add many events for (int i = 1; i <= 500; i++) @@ -347,8 +348,8 @@ TEST_F(AgentTest, CurrentAtOutOfRange) m_agentTestHelper->m_adapter->processData(line); } - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); { query["at"] = to_string(seq); @@ -358,7 +359,7 @@ TEST_F(AgentTest, CurrentAtOutOfRange) ASSERT_XML_PATH_EQUAL(doc, "//m:Error", line); } - seq = rest->getFirstSequence() - 1; + seq = circ.getFirstSequence() - 1; { query["at"] = to_string(seq); @@ -435,8 +436,8 @@ TEST_F(AgentTest, Composition) TEST_F(AgentTest, BadCount) { - auto rest = m_agentTestHelper->getRestService(); - int size = rest->getBufferSize() + 1; + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + int size = circ.getBufferSize() + 1; { QueryMap query {{"count", "NON_INTEGER"}}; PARSE_XML_RESPONSE_QUERY("/sample", query); @@ -535,8 +536,8 @@ TEST_F(AgentTest, SampleAtNextSeq) m_agentTestHelper->m_adapter->processData(line); } - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); { query["from"] = to_string(seq); PARSE_XML_RESPONSE_QUERY("/sample", query); @@ -548,8 +549,8 @@ TEST_F(AgentTest, SampleCount) { QueryMap query; addAdapter(); - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); // Get the current position char line[80] = {0}; @@ -595,8 +596,8 @@ TEST_F(AgentTest, SampleLastCount) m_agentTestHelper->m_adapter->processData(line); } - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence() - 20; + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence() - 20; { query["path"] = "//DataItem[@name='Xact']"; @@ -631,8 +632,8 @@ TEST_F(AgentTest, SampleToParameter) m_agentTestHelper->m_adapter->processData(line); } - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence() - 20; + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence() - 20; { query["path"] = "//DataItem[@name='Xact']"; @@ -694,8 +695,8 @@ TEST_F(AgentTest, EmptyStream) } { - auto rest = m_agentTestHelper->getRestService(); - QueryMap query {{"from", to_string(rest->getSequence())}}; + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + QueryMap query {{"from", to_string(circ.getSequence())}}; PARSE_XML_RESPONSE_QUERY("/sample", query); ASSERT_XML_PATH_EQUAL(doc, "//m:Streams", nullptr); } @@ -708,12 +709,12 @@ TEST_F(AgentTest, AddToBuffer) string device("LinuxCNC"), key("badKey"), value("ON"); SequenceNumber_t seqNum {0}; - auto rest = m_agentTestHelper->getRestService(); - auto event1 = rest->getFromBuffer(seqNum); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto event1 = circ.getFromBuffer(seqNum); ASSERT_FALSE(event1); { - query["from"] = to_string(rest->getSequence()); + query["from"] = to_string(circ.getSequence()); PARSE_XML_RESPONSE_QUERY("/sample", query); ASSERT_XML_PATH_EQUAL(doc, "//m:Streams", nullptr); } @@ -722,7 +723,7 @@ TEST_F(AgentTest, AddToBuffer) auto di2 = agent->getDataItemForDevice(device, key); seqNum = m_agentTestHelper->addToBuffer(di2, {{"VALUE", value}}, chrono::system_clock::now()); - auto event2 = rest->getFromBuffer(seqNum); + auto event2 = circ.getFromBuffer(seqNum); ASSERT_EQ(3, event2.use_count()); { @@ -744,9 +745,9 @@ TEST_F(AgentTest, SequenceNumberRollover) addAdapter(); // Set the sequence number near MAX_UINT32 - auto rest = m_agentTestHelper->getRestService(); - rest->setSequence(0xFFFFFFA0); - SequenceNumber_t seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + circ.setSequence(0xFFFFFFA0); + SequenceNumber_t seq = circ.getSequence(); ASSERT_EQ((int64_t)0xFFFFFFA0, seq); // Get the current position @@ -789,7 +790,7 @@ TEST_F(AgentTest, SequenceNumberRollover) } } - ASSERT_EQ(uint64_t(0xFFFFFFA0) + 128ul, rest->getSequence()); + ASSERT_EQ(uint64_t(0xFFFFFFA0) + 128ul, circ.getSequence()); #endif } @@ -2620,13 +2621,14 @@ TEST_F(AgentTest, StreamData) addAdapter(); auto heartbeatFreq {200ms}; auto rest = m_agentTestHelper->getRestService(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); rest->start(); // Start a thread... QueryMap query; query["interval"] = "50"; query["heartbeat"] = to_string(heartbeatFreq.count()); - query["from"] = to_string(rest->getSequence()); + query["from"] = to_string(circ.getSequence()); // Heartbeat test. Heartbeat should be sent in 200ms. Give // 25ms range. @@ -2688,19 +2690,21 @@ TEST_F(AgentTest, StreamDataObserver) auto rest = m_agentTestHelper->getRestService(); rest->start(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + // Start a thread... std::map query; query["interval"] = "100"; query["heartbeat"] = "1000"; query["count"] = "10"; - query["from"] = to_string(rest->getSequence()); + query["from"] = to_string(circ.getSequence()); query["path"] = "//DataItem[@name='line']"; // Test to make sure the signal will push the sequence number forward and capture // the new data. { PARSE_XML_STREAM_QUERY("/LinuxCNC/sample", query); - auto seq = to_string(rest->getSequence() + 20ull); + auto seq = to_string(circ.getSequence() + 20ull); for (int i = 0; i < 20; i++) { m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|block|" + to_string(i)); diff --git a/test/agent_test_helper.hpp b/test/agent_test_helper.hpp index c667453e..6289828e 100644 --- a/test/agent_test_helper.hpp +++ b/test/agent_test_helper.hpp @@ -256,7 +256,7 @@ class AgentTestHelper if (!IsOptionSet(options, configuration::Device)) { - options[configuration::Device] = *m_agent->defaultDevice()->getComponentName(); + options[configuration::Device] = *m_agent->getDefaultDevice()->getComponentName(); } boost::property_tree::ptree tree; tree.put(configuration::Host, host); diff --git a/test/config_test.cpp b/test/config_test.cpp index 08c75b6c..00d7b690 100644 --- a/test/config_test.cpp +++ b/test/config_test.cpp @@ -130,13 +130,11 @@ namespace { { m_config->loadConfig("BufferSize = 4\n"); - const auto agent = m_config->getAgent(); + auto agent = m_config->getAgent(); + auto &circ = agent->getCircularBuffer(); + ASSERT_TRUE(agent); - const auto sink = agent->findSink("RestService"); - ASSERT_TRUE(sink); - const auto rest = dynamic_pointer_cast(sink); - ASSERT_TRUE(rest); - ASSERT_EQ(16U, rest->getBufferSize()); + ASSERT_EQ(16U, circ.getBufferSize()); } TEST_F(ConfigTest, Device) diff --git a/test/data_set_test.cpp b/test/data_set_test.cpp index e8b78904..66b781d6 100644 --- a/test/data_set_test.cpp +++ b/test/data_set_test.cpp @@ -480,8 +480,8 @@ TEST_F(DataSetTest, CurrentAt) using namespace mtconnect::sink::rest_sink; m_agentTestHelper->addAdapter(); - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); m_agentTestHelper->m_adapter->processData("TIME|vars|a=1 b=2 c=3"); m_agentTestHelper->m_adapter->processData("TIME|vars| c=5 "); diff --git a/test/embedded_ruby_test.cpp b/test/embedded_ruby_test.cpp index f5290eff..944be9b2 100644 --- a/test/embedded_ruby_test.cpp +++ b/test/embedded_ruby_test.cpp @@ -445,7 +445,7 @@ p $source ASSERT_NE(nullptr, mrb); auto agent = m_config->getAgent(); - auto device = agent->defaultDevice(); + auto device = agent->getDefaultDevice(); ASSERT_TRUE(device); auto di = agent->getDataItemForDevice("000", "a"); diff --git a/test/mqtt_sink_test.cpp b/test/mqtt_sink_test.cpp index 213ad4c3..0e78f203 100644 --- a/test/mqtt_sink_test.cpp +++ b/test/mqtt_sink_test.cpp @@ -170,7 +170,7 @@ class MqttSinkTest : public testing::Test void addAdapter(ConfigOptions options = ConfigOptions {}) { m_agentTestHelper->addAdapter(options, "localhost", 7878, - m_agentTestHelper->m_agent->defaultDevice()->getName()); + m_agentTestHelper->m_agent->getDefaultDevice()->getName()); } std::unique_ptr m_jsonPrinter; diff --git a/test/pipeline_deliver_test.cpp b/test/pipeline_deliver_test.cpp index 6399837f..4d611663 100644 --- a/test/pipeline_deliver_test.cpp +++ b/test/pipeline_deliver_test.cpp @@ -67,11 +67,11 @@ class PipelineDeliverTest : public testing::Test TEST_F(PipelineDeliverTest, test_simple_flow) { m_agentTestHelper->addAdapter(); - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); m_agentTestHelper->m_adapter->processData("2021-01-22T12:33:45.123Z|Xpos|100.0"); - ASSERT_EQ(seq + 1, rest->getSequence()); - auto obs = rest->getFromBuffer(seq); + ASSERT_EQ(seq + 1, circ.getSequence()); + auto obs = circ.getFromBuffer(seq); ASSERT_TRUE(obs); ASSERT_EQ("Xpos", obs->getDataItem()->getName()); ASSERT_EQ(100.0, obs->getValue()); @@ -82,22 +82,22 @@ TEST_F(PipelineDeliverTest, filter_duplicates) { ConfigOptions options {{configuration::FilterDuplicates, true}}; m_agentTestHelper->addAdapter(options); - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); m_agentTestHelper->m_adapter->processData("2021-01-22T12:33:45.123Z|Xpos|100.0"); - ASSERT_EQ(seq + 1, rest->getSequence()); + ASSERT_EQ(seq + 1, circ.getSequence()); - auto obs = rest->getFromBuffer(seq); + auto obs = circ.getFromBuffer(seq); ASSERT_TRUE(obs); ASSERT_EQ("Xpos", obs->getDataItem()->getName()); ASSERT_EQ(100.0, obs->getValue()); m_agentTestHelper->m_adapter->processData("2021-01-22T12:33:45.123Z|Xpos|100.0"); - ASSERT_EQ(seq + 1, rest->getSequence()); + ASSERT_EQ(seq + 1, circ.getSequence()); m_agentTestHelper->m_adapter->processData("2021-01-22T12:33:45.123Z|Xpos|101.0"); - ASSERT_EQ(seq + 2, rest->getSequence()); - auto obs2 = rest->getFromBuffer(seq + 1); + ASSERT_EQ(seq + 2, circ.getSequence()); + auto obs2 = circ.getFromBuffer(seq + 1); ASSERT_EQ(101.0, obs2->getValue()); } @@ -106,18 +106,18 @@ TEST_F(PipelineDeliverTest, filter_upcase) { ConfigOptions options {{configuration::UpcaseDataItemValue, true}}; m_agentTestHelper->addAdapter(options); - auto rest = m_agentTestHelper->getRestService(); - auto seq = rest->getSequence(); + auto &circ = m_agentTestHelper->getAgent()->getCircularBuffer(); + auto seq = circ.getSequence(); m_agentTestHelper->m_adapter->processData("2021-01-22T12:33:45.123Z|a01c7f30|active"); - ASSERT_EQ(seq + 1, rest->getSequence()); + ASSERT_EQ(seq + 1, circ.getSequence()); - auto obs = rest->getFromBuffer(seq); + auto obs = circ.getFromBuffer(seq); ASSERT_TRUE(obs); ASSERT_EQ("a01c7f30", obs->getDataItem()->getId()); ASSERT_EQ("ACTIVE", obs->getValue()); m_agentTestHelper->m_adapter->processData("2021-01-22T12:33:45.123Z|Xpos|101.0"); - ASSERT_EQ(seq + 2, rest->getSequence()); - auto obs2 = rest->getFromBuffer(seq + 1); + ASSERT_EQ(seq + 2, circ.getSequence()); + auto obs2 = circ.getFromBuffer(seq + 1); ASSERT_EQ(101.0, obs2->getValue()); }