From d34ad3172099ea2ba60b421f778301e32c160ee5 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sat, 9 Mar 2024 18:12:33 -0500 Subject: [PATCH 1/3] added websocket support to the mqtt adapter and reformatted --- CMakeLists.txt | 2 +- README.md | 8 +- src/mtconnect/agent.cpp | 8 +- src/mtconnect/agent.hpp | 5 +- src/mtconnect/configuration/agent_config.cpp | 6 +- .../configuration/config_options.hpp | 1 + src/mtconnect/device_model/agent_device.cpp | 3 +- src/mtconnect/mqtt/mqtt_client_impl.hpp | 30 + src/mtconnect/printer/json_printer.cpp | 4 +- src/mtconnect/printer/printer.hpp | 6 +- src/mtconnect/sink/rest_sink/rest_service.cpp | 50 +- src/mtconnect/sink/sink.hpp | 7 +- .../source/adapter/mqtt/mqtt_adapter.cpp | 35 +- test_package/agent_device_test.cpp | 33 +- test_package/agent_test.cpp | 9 +- test_package/config_test.cpp | 1012 ++++++++--------- test_package/json_printer_probe_test.cpp | 1 - test_package/mqtt_sink_2_test.cpp | 19 +- 18 files changed, 650 insertions(+), 589 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 18db0cb75..656df8428 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 3) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 2) +set(AGENT_VERSION_BUILD 3) set(AGENT_VERSION_RC "") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent diff --git a/README.md b/README.md index 29c6cc856..8b4684f39 100755 --- a/README.md +++ b/README.md @@ -749,7 +749,7 @@ The following parameters must be present to enable https requests. If there is n * `TlsOnly` - Only allow secure connections, http requests will be rejected - *Default*: false + *Default*: `false` * `TlsPrivateKey` - The name of the file containing the private key for the certificate @@ -775,7 +775,11 @@ The following parameters must be present to enable https requests. If there is n * `MqttTls` - TLS Certificate for secure connection to the MQTT Broker - *Default*: *NULL* + *Default*: `false` + +* `MqttWs` - Instructs MQTT to connect using web sockets + + *Default*: `false` #### MQTT Sink diff --git a/src/mtconnect/agent.cpp b/src/mtconnect/agent.cpp index aec8b273e..59fad7ee9 100644 --- a/src/mtconnect/agent.cpp +++ b/src/mtconnect/agent.cpp @@ -113,7 +113,7 @@ namespace mtconnect { for (auto &[k, pr] : m_printers) pr->setSchemaVersion(*m_schemaVersion); } - + auto sender = GetOption(options, config::Sender); if (sender) { @@ -1301,15 +1301,15 @@ namespace mtconnect { // Validation methods // ----------------------------------------------- - string Agent::devicesAndPath(const std::optional &path, const DevicePtr device, const std::optional &deviceType) const + string Agent::devicesAndPath(const std::optional &path, const DevicePtr device, + const std::optional &deviceType) const { string dataPath; if (device || deviceType) { string prefix; - if ((device && device->getName() == "Agent") || - (deviceType && *deviceType == "Agent")) + if ((device && device->getName() == "Agent") || (deviceType && *deviceType == "Agent")) prefix = "//Devices/Agent"; else if (device) prefix = "//Devices/Device[@uuid=\"" + *device->getUuid() + "\"]"; diff --git a/src/mtconnect/agent.hpp b/src/mtconnect/agent.hpp index cd0387b27..a3202f3c9 100644 --- a/src/mtconnect/agent.hpp +++ b/src/mtconnect/agent.hpp @@ -427,8 +427,7 @@ namespace mtconnect { /// @param[in] device Optional device if one device is specified /// @param[in] deviceType optional Agent or Device selector /// @return The rewritten path properly prefixed - std::string devicesAndPath(const std::optional &path, - const DevicePtr device, + std::string devicesAndPath(const std::optional &path, const DevicePtr device, const std::optional &deviceType = std::nullopt) const; /// @brief Creates unique ids for the device model and maps to the originals @@ -644,7 +643,7 @@ namespace mtconnect { void getDataItemsForPath(const DevicePtr device, const std::optional &path, FilterSet &filter, - const std::optional &deviceType) const override + const std::optional &deviceType) const override { std::string dataPath = m_agent->devicesAndPath(path, device, deviceType); const auto &parser = m_agent->getXmlParser(); diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index 6d2ff7ba8..67ab3588f 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -856,7 +856,7 @@ namespace mtconnect::configuration { // Check for schema version auto port = get(options[configuration::Port]); LOG(info) << "Starting agent on port " << int(port); - + // Get the name of the sender auto sender = GetOption(options, configuration::Sender); if (sender) @@ -870,7 +870,7 @@ namespace mtconnect::configuration { if (ec) options[configuration::Sender] = "localhost"; else - options[configuration::Sender] = name; + options[configuration::Sender] = name; } // Make the Agent @@ -885,7 +885,7 @@ namespace mtconnect::configuration { m_agent->initialize(m_pipelineContext); m_version = *m_agent->getSchemaVersion(); - + DevicePtr device; if (IsOptionSet(options, configuration::PreserveUUID)) { diff --git a/src/mtconnect/configuration/config_options.hpp b/src/mtconnect/configuration/config_options.hpp index 00d563c7a..ac5bb2d3c 100644 --- a/src/mtconnect/configuration/config_options.hpp +++ b/src/mtconnect/configuration/config_options.hpp @@ -96,6 +96,7 @@ namespace mtconnect { DECLARE_CONFIGURATION(MqttTls); DECLARE_CONFIGURATION(MqttPort); DECLARE_CONFIGURATION(MqttHost); + DECLARE_CONFIGURATION(MqttWs); DECLARE_CONFIGURATION(MqttConnectInterval); DECLARE_CONFIGURATION(MqttUserName); DECLARE_CONFIGURATION(MqttPassword); diff --git a/src/mtconnect/device_model/agent_device.cpp b/src/mtconnect/device_model/agent_device.cpp index b48b5fed5..ef6df6022 100644 --- a/src/mtconnect/device_model/agent_device.cpp +++ b/src/mtconnect/device_model/agent_device.cpp @@ -45,7 +45,7 @@ namespace mtconnect { } return factory; } - + entity::FactoryPtr AgentDevice::getRoot() { static auto factory = make_shared( @@ -54,7 +54,6 @@ namespace mtconnect { return factory; } - AgentDevice::AgentDevice(const std::string &name, entity::Properties &props) : Device(name, props) { diff --git a/src/mtconnect/mqtt/mqtt_client_impl.hpp b/src/mtconnect/mqtt/mqtt_client_impl.hpp index 9ebb116fd..d29abae60 100644 --- a/src/mtconnect/mqtt/mqtt_client_impl.hpp +++ b/src/mtconnect/mqtt/mqtt_client_impl.hpp @@ -51,6 +51,8 @@ namespace mtconnect { using mqtt_client_ws_ptr = decltype(mqtt::make_async_client_ws(std::declval()...)); template using mqtt_tls_client_ws_ptr = decltype(mqtt::make_tls_async_client_ws(std::declval()...)); + template + using mqtt_client_ws_ptr = decltype(mqtt::make_async_client_ws(std::declval()...)); using mqtt_client = mqtt_client_ptr; @@ -59,6 +61,8 @@ namespace mtconnect { using mqtt_tls_client_ws = mqtt_tls_client_ws_ptr; + using mqtt_client_ws = mqtt_client_ws_ptr; /// @brief The Mqtt Client Source template @@ -504,5 +508,31 @@ namespace mtconnect { mqtt_tls_client_ws m_client; }; + /// @brief Create an Mqtt TLS WebSocket Client + class MqttWSClient : public MqttClientImpl + { + public: + using base = MqttClientImpl; + using base::base; + /// @brief Get the Mqtt TLS WebSocket Client + /// @return pointer to the Mqtt TLS WebSocket Client + auto &getClient() + { + if (!m_client) + { + m_client = mqtt::make_async_client_ws(m_ioContext, m_host, m_port); + if (m_username) + m_client->set_user_name(*m_username); + if (m_password) + m_client->set_password(*m_password); + } + + return m_client; + } + + protected: + mqtt_client_ws m_client; + }; + } // namespace mqtt_client } // namespace mtconnect diff --git a/src/mtconnect/printer/json_printer.cpp b/src/mtconnect/printer/json_printer.cpp index 77ca985cc..1108d0878 100644 --- a/src/mtconnect/printer/json_printer.cpp +++ b/src/mtconnect/printer/json_printer.cpp @@ -404,8 +404,8 @@ namespace mtconnect::printer { obj.AddPairs("jsonVersion", m_jsonVersion, "schemaVersion", *m_schemaVersion); { AutoJsonObject obj(writer, "Header"); - streamHeader(obj, m_version, m_senderName, instanceId, bufferSize, nextSeq, firstSeq, lastSeq, - *m_schemaVersion, m_modelChangeTime); + streamHeader(obj, m_version, m_senderName, instanceId, bufferSize, nextSeq, firstSeq, + lastSeq, *m_schemaVersion, m_modelChangeTime); } { diff --git a/src/mtconnect/printer/printer.hpp b/src/mtconnect/printer/printer.hpp index c69ea66ac..cfc0bd270 100644 --- a/src/mtconnect/printer/printer.hpp +++ b/src/mtconnect/printer/printer.hpp @@ -126,11 +126,11 @@ namespace mtconnect { /// @brief Get the schema version /// @return the schema version const auto &getSchemaVersion() const { return m_schemaVersion; } - + /// @brief sets the sener name for the header /// @param name the name of the sender void setSenderName(const std::string &s) { m_senderName = s; } - + /// @brief gets the sender name /// @returns the name of the sender in the header const auto &getSenderName() const { return m_senderName; } @@ -150,7 +150,7 @@ namespace mtconnect { bool m_pretty; std::string m_modelChangeTime; std::optional m_schemaVersion; - std::string m_senderName { "localhost" }; + std::string m_senderName {"localhost"}; }; } // namespace printer } // namespace mtconnect diff --git a/src/mtconnect/sink/rest_sink/rest_service.cpp b/src/mtconnect/sink/rest_sink/rest_service.cpp index 48379fcb0..6e123ed84 100644 --- a/src/mtconnect/sink/rest_sink/rest_service.cpp +++ b/src/mtconnect/sink/rest_sink/rest_service.cpp @@ -103,8 +103,9 @@ namespace mtconnect { {"removed", QUERY, "Boolean indicating if removed assets are included in results"}, {"type", QUERY, "Only include assets of type `type` in the results"}, {"count", QUERY, "Maximum number of entities to include in results"}, - {"assetId", QUERY, "An assetId to select"}, - {"deviceType", QUERY, "Values are 'Device' or 'Agent'. Selects only devices of that type."}, + {"assetId", QUERY, "An assetId to select"}, + {"deviceType", QUERY, + "Values are 'Device' or 'Agent'. Selects only devices of that type."}, {"assetId", PATH, "An assetId to select"}, {"path", QUERY, "XPath to filter DataItems matched against the probe document"}, {"at", QUERY, "Sequence number at which the observation snapshot is taken"}, @@ -477,7 +478,7 @@ namespace mtconnect { if (device && !ends_with(request->m_path, string("probe")) && m_sinkContract->findDeviceByUUIDorName(*device) == nullptr) return false; - + if (deviceType && *deviceType != "Device" && *deviceType != "Agent") { return false; @@ -487,24 +488,29 @@ namespace mtconnect { return true; }; - m_server->addRouting({boost::beast::http::verb::get, "/probe?pretty={bool:false}&deviceType={string}", handler}) + m_server + ->addRouting({boost::beast::http::verb::get, + "/probe?pretty={bool:false}&deviceType={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); m_server - ->addRouting( - {boost::beast::http::verb::get, "/{device}/probe?pretty={bool:false}&deviceType={string}", handler}) + ->addRouting({boost::beast::http::verb::get, + "/{device}/probe?pretty={bool:false}&deviceType={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " "device identified by `device` matching `name` or `uuid`."); // Must be last - m_server->addRouting({boost::beast::http::verb::get, "/?pretty={bool:false}&deviceType={string}", handler}) + m_server + ->addRouting( + {boost::beast::http::verb::get, "/?pretty={bool:false}&deviceType={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for all " "devices."); m_server - ->addRouting({boost::beast::http::verb::get, "/{device}?pretty={bool:false}&deviceType={string}", handler}) + ->addRouting({boost::beast::http::verb::get, + "/{device}?pretty={bool:false}&deviceType={string}", handler}) .document("MTConnect probe request", "Provides metadata service for the MTConnect Devices information model for " "device identified by `device` matching `name` or `uuid`."); @@ -648,11 +654,10 @@ namespace mtconnect { auto interval = request->parameter("interval"); if (interval) { - streamCurrentRequest(session, printerForAccepts(request->m_accepts), *interval, - request->parameter("device"), - request->parameter("path"), - *request->parameter("pretty"), - request->parameter("deviceType")); + streamCurrentRequest( + session, printerForAccepts(request->m_accepts), *interval, + request->parameter("device"), request->parameter("path"), + *request->parameter("pretty"), request->parameter("deviceType")); } else { @@ -696,13 +701,13 @@ namespace mtconnect { } else { - respond(session, - sampleRequest( - printerForAccepts(request->m_accepts), *request->parameter("count"), - request->parameter("device"), request->parameter("from"), - request->parameter("to"), request->parameter("path"), - *request->parameter("pretty"), - request->parameter("deviceType"))); + respond( + session, + sampleRequest( + printerForAccepts(request->m_accepts), *request->parameter("count"), + request->parameter("device"), request->parameter("from"), + request->parameter("to"), request->parameter("path"), + *request->parameter("pretty"), request->parameter("deviceType"))); } return true; }; @@ -787,9 +792,8 @@ namespace mtconnect { deviceList = m_sinkContract->getDevices(); if (deviceType) { - deviceList.remove_if([&deviceType](const DevicePtr &dev) { - return dev->getName() != *deviceType; - }); + deviceList.remove_if( + [&deviceType](const DevicePtr &dev) { return dev->getName() != *deviceType; }); } } diff --git a/src/mtconnect/sink/sink.hpp b/src/mtconnect/sink/sink.hpp index 5eb44ed27..1e517284d 100644 --- a/src/mtconnect/sink/sink.hpp +++ b/src/mtconnect/sink/sink.hpp @@ -86,10 +86,9 @@ namespace mtconnect { /// @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 std::optional &deviceType = std::nullopt) const = 0; + virtual void getDataItemsForPath( + const DevicePtr device, const std::optional &path, FilterSet &filter, + const std::optional &deviceType = std::nullopt) const = 0; /// @brief Add a source for this sink. /// /// This is used to create loopback sources for a sink diff --git a/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp b/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp index 5e42806e2..09e5915c7 100644 --- a/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp +++ b/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp @@ -73,8 +73,8 @@ namespace mtconnect { {configuration::MqttHost, string()}}); AddDefaultedOptions(block, m_options, - {{configuration::MqttPort, 1883}, - {configuration::MqttTls, false}, + {{configuration::MqttTls, false}, + {configuration::MqttWs, false}, {configuration::AutoAvailable, false}, {configuration::RealTime, false}, {configuration::RelativeTime, false}}); @@ -85,6 +85,15 @@ namespace mtconnect { { m_options[configuration::MqttHost] = m_options[configuration::Host]; } + if (!HasOption(m_options, configuration::MqttPort) && + HasOption(m_options, configuration::Port)) + { + m_options[configuration::MqttPort] = m_options[configuration::Port]; + } + else + { + m_options[configuration::MqttPort] = 1883; + } m_handler = m_pipeline.makeHandler(); auto clientHandler = make_unique(); @@ -110,12 +119,28 @@ namespace mtconnect { m_handler->m_processMessage(topic, payload, client->getIdentity()); }; - if (IsOptionSet(m_options, configuration::MqttTls)) + if (IsOptionSet(m_options, configuration::MqttTls) && + !IsOptionSet(m_options, configuration::MqttWs)) + { m_client = make_shared(m_ioContext, m_options, - std::move(clientHandler)); + move(clientHandler)); + } + else if (IsOptionSet(m_options, configuration::MqttWs) && + IsOptionSet(m_options, configuration::MqttTls)) + { + m_client = make_shared(m_ioContext, m_options, + std::move(clientHandler)); + } + else if (IsOptionSet(m_options, configuration::MqttWs)) + { + m_client = make_shared(m_ioContext, m_options, + std::move(clientHandler)); + } else + { m_client = make_shared(m_ioContext, m_options, - std::move(clientHandler)); + move(clientHandler)); + } m_identity = m_client->getIdentity(); m_name = m_client->getUrl(); diff --git a/test_package/agent_device_test.cpp b/test_package/agent_device_test.cpp index 7fbd7fec1..02a131dfa 100644 --- a/test_package/agent_device_test.cpp +++ b/test_package/agent_device_test.cpp @@ -328,73 +328,76 @@ TEST_F(AgentDeviceTest, verify_uuid_can_be_set_in_configuration) ASSERT_EQ("HELLO_KITTY", *m_agentDevice->getUuid()); } -/// @test validate the use of deviceType rest parameter to select only the Agent or Devices for probe +/// @test validate the use of deviceType rest parameter to select only the Agent or Devices for +/// probe TEST_F(AgentDeviceTest, should_only_return_only_devices_of_device_type_for_probe) { using namespace mtconnect::sink::rest_sink; - + m_port = 21788; addAdapter(); { QueryMap query {{"deviceType", "Agent"}}; PARSE_XML_RESPONSE_QUERY("/probe", query); - + ASSERT_XML_PATH_COUNT(doc, "//m:Device", 0); ASSERT_XML_PATH_COUNT(doc, "//m:Agent", 1); } - + { QueryMap query {{"deviceType", "Device"}}; PARSE_XML_RESPONSE_QUERY("/probe", query); - + ASSERT_XML_PATH_COUNT(doc, "//m:Device", 1); ASSERT_XML_PATH_COUNT(doc, "//m:Agent", 0); } } -/// @test validate the use of deviceType rest parameter to select only the Agent or Devices for current +/// @test validate the use of deviceType rest parameter to select only the Agent or Devices for +/// current TEST_F(AgentDeviceTest, should_only_return_only_devices_of_device_type_for_current) { using namespace mtconnect::sink::rest_sink; - + m_port = 21788; addAdapter(); { QueryMap query {{"deviceType", "Agent"}}; PARSE_XML_RESPONSE_QUERY("/current", query); - + ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='Agent']", 1); ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='LinuxCNC']", 0); } - + { QueryMap query {{"deviceType", "Device"}}; PARSE_XML_RESPONSE_QUERY("/current", query); - + ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='Agent']", 0); ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='LinuxCNC']", 1); } } -/// @test validate the use of deviceType rest parameter to select only the Agent or Devices for sample +/// @test validate the use of deviceType rest parameter to select only the Agent or Devices for +/// sample TEST_F(AgentDeviceTest, should_only_return_only_devices_of_device_type_for_sample) { using namespace mtconnect::sink::rest_sink; - + m_port = 21788; addAdapter(); { QueryMap query {{"deviceType", "Agent"}}; PARSE_XML_RESPONSE_QUERY("/sample", query); - + ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='Agent']", 1); ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='LinuxCNC']", 0); } - + { QueryMap query {{"deviceType", "Device"}}; PARSE_XML_RESPONSE_QUERY("/sample", query); - + ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='Agent']", 0); ASSERT_XML_PATH_COUNT(doc, "//m:DeviceStream[@name='LinuxCNC']", 1); } diff --git a/test_package/agent_test.cpp b/test_package/agent_test.cpp index ea6326096..b17dd87ac 100644 --- a/test_package/agent_test.cpp +++ b/test_package/agent_test.cpp @@ -3026,19 +3026,16 @@ TEST_F(AgentTest, should_not_add_spaces_to_output) TEST_F(AgentTest, should_set_sender_from_config_in_XML_header) { - auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 4, false, true, {{configuration::Sender, "MachineXXX"s}}); + auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 4, false, + true, {{configuration::Sender, "MachineXXX"s}}); ASSERT_TRUE(agent); { PARSE_XML_RESPONSE("/probe"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); } - + { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@sender", "MachineXXX"); } - - } - - diff --git a/test_package/config_test.cpp b/test_package/config_test.cpp index 2920e5696..296931c9f 100644 --- a/test_package/config_test.cpp +++ b/test_package/config_test.cpp @@ -64,17 +64,17 @@ namespace { m_config = std::make_unique(); m_config->setDebug(true); m_cwd = std::filesystem::current_path(); - + chdir(TEST_BIN_ROOT_DIR); m_config->updateWorkingDirectory(); } - + void TearDown() override { m_config.reset(); chdir(m_cwd.string().c_str()); } - + fs::path createTempDirectory(const string &ext) { fs::path root {fs::path(TEST_BIN_ROOT_DIR) / ("config_test_" + ext)}; @@ -82,33 +82,33 @@ namespace { { fs::remove_all(root); } - + fs::create_directory(root); chdir(root.string().c_str()); m_config->updateWorkingDirectory(); // m_config->setDebug(false); - + return root; } - + fs::path copySampleFile(const std::string &src, fs::path target, chrono::seconds delta) { fs::path file {fs::path("samples") / src}; return copyFile(file, target, delta); } - + fs::path copyFile(const fs::path src, fs::path target, chrono::seconds delta) { fs::path file {fs::path(TEST_RESOURCE_DIR) / src}; - + fs::copy_file(file, target, fs::copy_options::overwrite_existing); auto t = fs::last_write_time(target); if (delta.count() != 0) fs::last_write_time(target, t - delta); - + return target; } - + void replaceTextInFile(fs::path file, const std::string &from, const std::string &to) { ifstream is {file.string(), ios::binary | ios::ate}; @@ -117,65 +117,65 @@ namespace { is.seekg(0); is.read(&str[0], size); is.close(); - + replace_all(str, from, to); - + ofstream os(file.string()); os << str; os.close(); } - + std::unique_ptr m_config; std::filesystem::path m_cwd; }; - + TEST_F(ConfigTest, BlankConfig) { m_config->loadConfig(""); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); ASSERT_EQ(size_t(1), agent->getDevices().size()); ASSERT_EQ("1.1", *agent->getSchemaVersion()); } - + TEST_F(ConfigTest, BufferSize) { m_config->loadConfig("BufferSize = 4\n"); - + auto agent = m_config->getAgent(); auto &circ = agent->getCircularBuffer(); - + ASSERT_TRUE(agent); ASSERT_EQ(16U, circ.getBufferSize()); } - + TEST_F(ConfigTest, Device) { string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto source = agent->getSources().back(); const auto adapter = dynamic_pointer_cast(source); - + auto deviceName = GetOption(adapter->getOptions(), configuration::Device); ASSERT_TRUE(deviceName); ASSERT_EQ("LinuxCNC", *deviceName); - + ASSERT_FALSE(IsOptionSet(adapter->getOptions(), configuration::FilterDuplicates)); ASSERT_FALSE(IsOptionSet(adapter->getOptions(), configuration::AutoAvailable)); ASSERT_FALSE(IsOptionSet(adapter->getOptions(), configuration::IgnoreTimestamps)); - + auto device = agent->findDeviceByUUIDorName(*deviceName); ASSERT_TRUE(device->preserveUuid()); } - + TEST_F(ConfigTest, Adapter) { using namespace std::chrono_literals; - + string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n" "Adapters { LinuxCNC { \n" @@ -188,38 +188,38 @@ namespace { "LegacyTimeout = 2000\n" "} }\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto source = agent->getSources().back(); const auto adapter = dynamic_pointer_cast(source); - + ASSERT_EQ(23, (int)adapter->getPort()); ASSERT_EQ(std::string("10.211.55.1"), adapter->getServer()); ASSERT_TRUE(IsOptionSet(adapter->getOptions(), configuration::FilterDuplicates)); ASSERT_TRUE(IsOptionSet(adapter->getOptions(), configuration::AutoAvailable)); ASSERT_TRUE(IsOptionSet(adapter->getOptions(), configuration::IgnoreTimestamps)); - + ASSERT_EQ(2000s, adapter->getLegacyTimeout()); - + // TODO: Need to link to device to the adapter. // ASSERT_TRUE(device->m_preserveUuid); } - + TEST_F(ConfigTest, DefaultPreserveUUID) { string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n" "PreserveUUID = true\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto device = agent->getDevices().front(); - + ASSERT_TRUE(device->preserveUuid()); } - + TEST_F(ConfigTest, DefaultPreserveOverride) { string str("Devices = " TEST_RESOURCE_DIR @@ -229,96 +229,96 @@ namespace { "PreserveUUID = false\n" "} }\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto device = agent->findDeviceByUUIDorName("LinuxCNC"); - + ASSERT_FALSE(device->preserveUuid()); } - + TEST_F(ConfigTest, DisablePut) { string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n" "AllowPut = true\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto sink = agent->findSink("RestService"); ASSERT_TRUE(sink); const auto rest = dynamic_pointer_cast(sink); - + ASSERT_TRUE(rest->getServer()->arePutsAllowed()); } - + TEST_F(ConfigTest, LimitPut) { string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n" "AllowPutFrom = localhost\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto sink = agent->findSink("RestService"); ASSERT_TRUE(sink); const auto rest = dynamic_pointer_cast(sink); ASSERT_TRUE(rest); - + ASSERT_TRUE(rest->getServer()->arePutsAllowed()); ASSERT_TRUE(rest->getServer()->allowPutFrom(std::string("127.0.0.1"))); } - + TEST_F(ConfigTest, LimitPutFromHosts) { string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n" "AllowPutFrom = localhost, 192.168.0.1\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto sink = agent->findSink("RestService"); ASSERT_TRUE(sink); const auto rest = dynamic_pointer_cast(sink); ASSERT_TRUE(rest); - + ASSERT_TRUE(rest->getServer()->arePutsAllowed()); ASSERT_TRUE(rest->getServer()->allowPutFrom(std::string("127.0.0.1"))); ASSERT_TRUE(rest->getServer()->allowPutFrom(std::string("192.168.0.1"))); } - + TEST_F(ConfigTest, Namespaces) { string streams( - "StreamsNamespaces {\n" - "x {\n" - "Urn = urn:example.com:ExampleStreams:1.2\n" - "Location = /schemas/ExampleStreams_1.2.xsd\n" - "Path = ./ExampleStreams_1.2.xsd\n" - "}\n" - "}\n"); - + "StreamsNamespaces {\n" + "x {\n" + "Urn = urn:example.com:ExampleStreams:1.2\n" + "Location = /schemas/ExampleStreams_1.2.xsd\n" + "Path = ./ExampleStreams_1.2.xsd\n" + "}\n" + "}\n"); + m_config->loadConfig(streams); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); auto printer = dynamic_cast(agent->getPrinter("xml")); ASSERT_TRUE(printer); - + auto path = printer->getStreamsUrn("x"); ASSERT_EQ(std::string("urn:example.com:ExampleStreams:1.2"), path); - + string devices( - "DevicesNamespaces {\n" - "y {\n" - "Urn = urn:example.com:ExampleDevices:1.2\n" - "Location = /schemas/ExampleDevices_1.2.xsd\n" - "Path = ./ExampleDevices_1.2.xsd\n" - "}\n" - "}\n"); - + "DevicesNamespaces {\n" + "y {\n" + "Urn = urn:example.com:ExampleDevices:1.2\n" + "Location = /schemas/ExampleDevices_1.2.xsd\n" + "Path = ./ExampleDevices_1.2.xsd\n" + "}\n" + "}\n"); + m_config->loadConfig(devices); agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); @@ -326,16 +326,16 @@ namespace { ASSERT_TRUE(printer); path = printer->getDevicesUrn("y"); ASSERT_EQ(std::string("urn:example.com:ExampleDevices:1.2"), path); - + string asset( - "AssetsNamespaces {\n" - "z {\n" - "Urn = urn:example.com:ExampleAssets:1.2\n" - "Location = /schemas/ExampleAssets_1.2.xsd\n" - "Path = ./ExampleAssets_1.2.xsd\n" - "}\n" - "}\n"); - + "AssetsNamespaces {\n" + "z {\n" + "Urn = urn:example.com:ExampleAssets:1.2\n" + "Location = /schemas/ExampleAssets_1.2.xsd\n" + "Path = ./ExampleAssets_1.2.xsd\n" + "}\n" + "}\n"); + m_config->loadConfig(asset); agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); @@ -343,16 +343,16 @@ namespace { ASSERT_TRUE(printer); path = printer->getAssetsUrn("z"); ASSERT_EQ(std::string("urn:example.com:ExampleAssets:1.2"), path); - + string errors( - "ErrorNamespaces {\n" - "a {\n" - "Urn = urn:example.com:ExampleErrors:1.2\n" - "Location = /schemas/ExampleErrors_1.2.xsd\n" - "Path = ./ExampleErrorss_1.2.xsd\n" - "}\n" - "}\n"); - + "ErrorNamespaces {\n" + "a {\n" + "Urn = urn:example.com:ExampleErrors:1.2\n" + "Location = /schemas/ExampleErrors_1.2.xsd\n" + "Path = ./ExampleErrorss_1.2.xsd\n" + "}\n" + "}\n"); + m_config->loadConfig(errors); agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); @@ -361,37 +361,37 @@ namespace { path = printer->getErrorUrn("a"); ASSERT_EQ(std::string("urn:example.com:ExampleErrors:1.2"), path); } - + TEST_F(ConfigTest, LegacyTimeout) { using namespace std::chrono_literals; - + string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n" "LegacyTimeout = 2000\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); const auto source = agent->getSources().back(); const auto adapter = dynamic_pointer_cast(source); - + ASSERT_EQ(2000s, adapter->getLegacyTimeout()); } - + TEST_F(ConfigTest, IgnoreTimestamps) { string str("Devices = " TEST_RESOURCE_DIR "/samples/test_config.xml\n" "IgnoreTimestamps = true\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); const auto source = agent->getSources().back(); const auto adapter = dynamic_pointer_cast(source); - + ASSERT_TRUE(IsOptionSet(adapter->getOptions(), configuration::IgnoreTimestamps)); } - + TEST_F(ConfigTest, IgnoreTimestampsOverride) { string str("Devices = " TEST_RESOURCE_DIR @@ -401,124 +401,124 @@ namespace { "IgnoreTimestamps = false\n" "} }\n"); m_config->loadConfig(str); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto source = agent->getSources().back(); const auto adapter = dynamic_pointer_cast(source); - + ASSERT_FALSE(IsOptionSet(adapter->getOptions(), configuration::IgnoreTimestamps)); } - + TEST_F(ConfigTest, SpecifyMTCNamespace) { string streams( - "StreamsNamespaces {\n" - "m {\n" - "Location = /schemas/MTConnectStreams_1.2.xsd\n" - "Path = ./MTConnectStreams_1.2.xsd\n" - "}\n" - "}\n"); - + "StreamsNamespaces {\n" + "m {\n" + "Location = /schemas/MTConnectStreams_1.2.xsd\n" + "Path = ./MTConnectStreams_1.2.xsd\n" + "}\n" + "}\n"); + m_config->loadConfig(streams); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); auto printer = dynamic_cast(agent->getPrinter("xml")); ASSERT_TRUE(printer); - + auto path = printer->getStreamsUrn("m"); ASSERT_EQ(std::string(""), path); auto location = printer->getStreamsLocation("m"); ASSERT_EQ(std::string("/schemas/MTConnectStreams_1.2.xsd"), location); - + printer->clearStreamsNamespaces(); } - + TEST_F(ConfigTest, SetSchemaVersion) { string streams("SchemaVersion = 1.4\n"); - + m_config->loadConfig(streams); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); auto printer = dynamic_cast(agent->getPrinter("xml")); ASSERT_TRUE(printer); - + auto version = printer->getSchemaVersion(); ASSERT_EQ(std::string("1.4"), version); - + printer->setSchemaVersion("1.3"); } - + TEST_F(ConfigTest, SchemaDirectory) { string schemas( - "SchemaVersion = 1.3\n" - "Files {\n" - "schemas {\n" - "Location = /schemas\n" - "Path = " PROJECT_ROOT_DIR - "/schemas\n" - "}\n" - "}\n" - "logger_config {\n" - "output = cout\n" - "}\n"); - + "SchemaVersion = 1.3\n" + "Files {\n" + "schemas {\n" + "Location = /schemas\n" + "Path = " PROJECT_ROOT_DIR + "/schemas\n" + "}\n" + "}\n" + "logger_config {\n" + "output = cout\n" + "}\n"); + m_config->setDebug(true); m_config->loadConfig(schemas); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); auto printer = dynamic_cast(agent->getPrinter("xml")); ASSERT_TRUE(printer); - + auto path = printer->getStreamsUrn("m"); ASSERT_EQ(std::string("urn:mtconnect.org:MTConnectStreams:1.3"), path); auto location = printer->getStreamsLocation("m"); ASSERT_EQ(std::string("/schemas/MTConnectStreams_1.3.xsd"), location); - + path = printer->getDevicesUrn("m"); ASSERT_EQ(std::string("urn:mtconnect.org:MTConnectDevices:1.3"), path); location = printer->getDevicesLocation("m"); ASSERT_EQ(std::string("/schemas/MTConnectDevices_1.3.xsd"), location); - + path = printer->getAssetsUrn("m"); ASSERT_EQ(std::string("urn:mtconnect.org:MTConnectAssets:1.3"), path); location = printer->getAssetsLocation("m"); ASSERT_EQ(std::string("/schemas/MTConnectAssets_1.3.xsd"), location); - + path = printer->getErrorUrn("m"); ASSERT_EQ(std::string("urn:mtconnect.org:MTConnectError:1.3"), path); location = printer->getErrorLocation("m"); ASSERT_EQ(std::string("/schemas/MTConnectError_1.3.xsd"), location); } - + TEST_F(ConfigTest, check_http_headers) { string str( - "HttpHeaders {\n" - " Access-Control-Allow-Origin = *\n" - "\n" - "}\n"); + "HttpHeaders {\n" + " Access-Control-Allow-Origin = *\n" + "\n" + "}\n"); m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); - + ASSERT_TRUE(agent); const auto sink = agent->findSink("RestService"); ASSERT_TRUE(sink); const auto rest = dynamic_pointer_cast(sink); ASSERT_TRUE(rest); const auto server = rest->getServer(); - + const auto &headers = server->getHttpHeaders(); - + ASSERT_EQ(1, headers.size()); const auto &first = headers.front(); ASSERT_EQ("Access-Control-Allow-Origin", first.first); ASSERT_EQ(" *", first.second); } - + TEST_F(ConfigTest, dynamic_load_sinks_bad) { string str(R"( @@ -531,15 +531,15 @@ Sinks { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto sink = agent->findSink("TestBADService"); ASSERT_TRUE(sink == nullptr); } - + TEST_F(ConfigTest, dynamic_load_sinks_simple) { string str(R"( @@ -548,16 +548,16 @@ Sinks { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); - + const auto sink = agent->findSink("sink_plugin_test"); ASSERT_TRUE(sink != nullptr); } - + TEST_F(ConfigTest, dynamic_load_sinks_with_plugin_block) { string str(R"( @@ -570,16 +570,16 @@ Sinks { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); - + const auto sink = agent->findSink("sink_plugin_test"); ASSERT_TRUE(sink != nullptr); } - + TEST_F(ConfigTest, dynamic_load_sinks_assigned_name) { string str(R"( @@ -588,18 +588,18 @@ Sinks { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto sink1 = agent->findSink("sink_plugin_test"); ASSERT_TRUE(sink1 == nullptr); - + const auto sink2 = agent->findSink("Sink1"); ASSERT_TRUE(sink2 != nullptr); } - + TEST_F(ConfigTest, dynamic_load_sinks_assigned_name_tag) { string str(R"( @@ -609,18 +609,18 @@ Sinks { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto sink1 = agent->findSink("sink_plugin_test"); ASSERT_TRUE(sink1 == nullptr); - + const auto sink2 = agent->findSink("Sink1"); ASSERT_TRUE(sink2 != nullptr); } - + // TEST_F(ConfigTest, dynamic_load_adapter_bad) { @@ -632,15 +632,15 @@ Adapters { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto adapter = agent->findSource("_Host1_7878"); ASSERT_TRUE(adapter == nullptr); } - + TEST_F(ConfigTest, dynamic_load_adapter_simple) { string str(R"( @@ -651,15 +651,15 @@ Adapters { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto adapter = agent->findSource("Test"); ASSERT_TRUE(adapter != nullptr); } - + TEST_F(ConfigTest, dynamic_load_adapter_with_plugin_block) { string str(R"( @@ -675,256 +675,256 @@ Adapters { } } )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto adapter = agent->findSource("Test"); ASSERT_TRUE(adapter != nullptr); } - + TEST_F(ConfigTest, max_cache_size_in_no_units) { string str(R"( MaxCachedFileSize = 2000 )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto rest = - dynamic_pointer_cast(agent->findSink("RestService")); + dynamic_pointer_cast(agent->findSink("RestService")); ASSERT_TRUE(rest != nullptr); - + auto cache = rest->getFileCache(); ASSERT_EQ(2000, cache->getMaxCachedFileSize()); } - + TEST_F(ConfigTest, max_cache_size_in_kb) { string str(R"( MaxCachedFileSize = 2k )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto rest = - dynamic_pointer_cast(agent->findSink("RestService")); + dynamic_pointer_cast(agent->findSink("RestService")); ASSERT_TRUE(rest != nullptr); - + auto cache = rest->getFileCache(); ASSERT_EQ(2048, cache->getMaxCachedFileSize()); } - + TEST_F(ConfigTest, max_cache_size_in_Kb_in_uppercase) { string str(R"( MaxCachedFileSize = 2K )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto rest = - dynamic_pointer_cast(agent->findSink("RestService")); + dynamic_pointer_cast(agent->findSink("RestService")); ASSERT_TRUE(rest != nullptr); - + auto cache = rest->getFileCache(); ASSERT_EQ(2048, cache->getMaxCachedFileSize()); } - + TEST_F(ConfigTest, max_cache_size_in_mb) { string str(R"( MaxCachedFileSize = 2m )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto rest = - dynamic_pointer_cast(agent->findSink("RestService")); + dynamic_pointer_cast(agent->findSink("RestService")); ASSERT_TRUE(rest != nullptr); - + auto cache = rest->getFileCache(); ASSERT_EQ(2 * 1024 * 1024, cache->getMaxCachedFileSize()); } - + TEST_F(ConfigTest, max_cache_size_in_gb) { string str(R"( MaxCachedFileSize = 2g )"); - + m_config->loadConfig(str); auto agent = const_cast(m_config->getAgent()); - + ASSERT_TRUE(agent); const auto rest = - dynamic_pointer_cast(agent->findSink("RestService")); + dynamic_pointer_cast(agent->findSink("RestService")); ASSERT_TRUE(rest != nullptr); - + auto cache = rest->getFileCache(); ASSERT_EQ(2ull * 1024 * 1024 * 1024, cache->getMaxCachedFileSize()); } - + #define EXPECT_PATH_EQ(p1, p2) \ -EXPECT_EQ(std::filesystem::weakly_canonical(p1), std::filesystem::weakly_canonical(p2)) - + EXPECT_EQ(std::filesystem::weakly_canonical(p1), std::filesystem::weakly_canonical(p2)) + TEST_F(ConfigTest, log_output_should_set_archive_file_pattern) { m_config->setDebug(false); - + string str(R"( logger_config { output = file agent.log } )"); - + m_config->loadConfig(str); - + auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - + EXPECT_EQ("agent_%Y-%m-%d_%H-%M-%S_%N.log", m_config->getLogArchivePattern().filename()); EXPECT_EQ("agent.log", m_config->getLogFileName().filename()); EXPECT_PATH_EQ(TEST_BIN_ROOT_DIR, m_config->getLogDirectory()); } - + TEST_F(ConfigTest, log_output_should_configure_file_name) { m_config->setDebug(false); - + string str(R"( logger_config { output = file logging.log logging_%N.log } )"); - + m_config->loadConfig(str); - + auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - + EXPECT_EQ("logging_%N.log", m_config->getLogArchivePattern().filename()); EXPECT_EQ("logging.log", m_config->getLogFileName().filename()); EXPECT_PATH_EQ(TEST_BIN_ROOT_DIR, m_config->getLogDirectory()); } - + TEST_F(ConfigTest, log_should_configure_file_name) { m_config->setDebug(false); - + string str(R"( logger_config { file_name = logging.log archive_pattern = logging_%N.log } )"); - + m_config->loadConfig(str); - + auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - + EXPECT_EQ("logging_%N.log", m_config->getLogArchivePattern().filename()); EXPECT_EQ("logging.log", m_config->getLogFileName().filename()); EXPECT_PATH_EQ(TEST_BIN_ROOT_DIR, m_config->getLogDirectory()); } - + TEST_F(ConfigTest, log_should_specify_relative_directory) { m_config->setDebug(false); - + string str(R"( logger_config { file_name = logging.log archive_pattern = logs/logging_%N.log } )"); - + m_config->loadConfig(str); - + auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - + fs::path path {std::filesystem::canonical(TEST_BIN_ROOT_DIR) / "logs"}; - + EXPECT_PATH_EQ(path / "logging_%N.log", m_config->getLogArchivePattern()); EXPECT_PATH_EQ(path / "logging.log", m_config->getLogFileName()); EXPECT_PATH_EQ(path, m_config->getLogDirectory()); } - + TEST_F(ConfigTest, log_should_specify_relative_directory_with_active_in_parent) { m_config->setDebug(false); - + string str(R"( logger_config { file_name = ./logging.log archive_pattern = logs/logging_%N.log } )"); - + m_config->loadConfig(str); - + auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - + fs::path path {std::filesystem::canonical(TEST_BIN_ROOT_DIR)}; - + EXPECT_PATH_EQ(path / "logs" / "logging_%N.log", m_config->getLogArchivePattern()); EXPECT_PATH_EQ(path / "logging.log", m_config->getLogFileName()); EXPECT_PATH_EQ(path / "logs", m_config->getLogDirectory()); } - + TEST_F(ConfigTest, log_should_specify_max_file_and_rotation_size) { m_config->setDebug(false); using namespace boost::log::trivial; - + string str(R"( logger_config { max_size = 1gb rotation_size = 20gb } )"); - + m_config->loadConfig(str); - + auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - + EXPECT_EQ(severity_level::info, m_config->getLogLevel()); EXPECT_EQ(1ll * 1024 * 1024 * 1024, m_config->getMaxLogFileSize()); EXPECT_EQ(20ll * 1024 * 1024 * 1024, m_config->getLogRotationSize()); } - + TEST_F(ConfigTest, log_should_configure_logging_level) { m_config->setDebug(false); - + using namespace boost::log::trivial; - + string str(R"( logger_config { level = fatal } )"); - + m_config->loadConfig(str); - + auto sink = m_config->getLoggerSink(); ASSERT_TRUE(sink); - + EXPECT_EQ(severity_level::fatal, m_config->getLogLevel()); - + m_config->setLoggingLevel("all"); EXPECT_EQ(severity_level::trace, m_config->getLogLevel()); m_config->setLoggingLevel("none"); @@ -945,7 +945,7 @@ logger_config { EXPECT_EQ(severity_level::error, m_config->getLogLevel()); m_config->setLoggingLevel("fatal"); EXPECT_EQ(severity_level::fatal, m_config->getLogLevel()); - + m_config->setLoggingLevel("ALL"); EXPECT_EQ(severity_level::trace, m_config->getLogLevel()); m_config->setLoggingLevel("NONE"); @@ -967,11 +967,11 @@ logger_config { m_config->setLoggingLevel("FATAL"); EXPECT_EQ(severity_level::fatal, m_config->getLogLevel()); } - + TEST_F(ConfigTest, should_reload_device_xml_file) { auto root {createTempDirectory("1")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -984,27 +984,27 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("min_config.xml", devices, 60min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &context = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto chg = printer->getModelChangeTime(); auto device = agent->getDeviceByName("LinuxCNC"); - + auto dataItem = device->getDeviceDataItem("c1"); ASSERT_TRUE(dataItem); ASSERT_EQ("SPINDLE_SPEED", dataItem->getType()); - + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices, agent](boost::system::error_code ec) { @@ -1018,12 +1018,12 @@ Port = 0 EXPECT_TRUE(di); EXPECT_EQ("SPINDLE_SPEED", di->getType()); di.reset(); - + // Modify devices replaceTextInFile(devices, "SPINDLE_SPEED", "ROTARY_VELOCITY"); } }); - + boost::asio::steady_timer timer2(context.get()); timer2.expires_from_now(6s); timer2.async_wait([this, agent, &chg](boost::system::error_code ec) { @@ -1033,9 +1033,9 @@ Port = 0 auto dataItem = agent->getDataItemById("c1"); EXPECT_TRUE(dataItem); EXPECT_EQ("ROTARY_VELOCITY", dataItem->getType()); - + EXPECT_FALSE(dataItem->isOrphan()); - + auto agent = m_config->getAgent(); const auto &printer = agent->getPrinter("xml"); EXPECT_NE(nullptr, printer); @@ -1043,14 +1043,14 @@ Port = 0 } m_config->stop(); }); - + m_config->start(); } - + TEST_F(ConfigTest, should_reload_device_xml_and_skip_unchanged_devices) { fs::path root {createTempDirectory("2")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1063,27 +1063,27 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("min_config.xml", devices, 1min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &context = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto chg = printer->getModelChangeTime(); auto device = agent->getDeviceByName("LinuxCNC"); - + auto dataItem = device->getDeviceDataItem("c1"); ASSERT_TRUE(dataItem); ASSERT_EQ("SPINDLE_SPEED", dataItem->getType()); - + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices](boost::system::error_code ec) { @@ -1096,7 +1096,7 @@ Port = 0 fs::last_write_time(devices, fs::file_time_type::clock::now()); } }); - + boost::asio::steady_timer timer2(context.get()); timer2.expires_from_now(6s); timer2.async_wait([this, &chg](boost::system::error_code ec) { @@ -1109,15 +1109,15 @@ Port = 0 } m_config->stop(); }); - + m_config->start(); } - + TEST_F(ConfigTest, should_restart_agent_when_config_file_changes) { fs::path root {createTempDirectory("3")}; auto &context = m_config->getAsyncContext(); - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1130,26 +1130,26 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("min_config.xml", devices, 0s); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + auto t = fs::last_write_time(config); fs::last_write_time(config, t - 1min); - + m_config->initialize(options); - + auto agent = m_config->getAgent(); const auto sink = agent->findSink("RestService"); ASSERT_TRUE(sink); const auto rest = dynamic_pointer_cast(sink); ASSERT_TRUE(rest); - + auto instance = rest->instanceId(); - + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &config](boost::system::error_code ec) { @@ -1162,10 +1162,10 @@ Port = 0 fs::last_write_time(config, fs::file_time_type::clock::now()); } }); - + auto th = thread([this, agent, instance, &context]() { this_thread::sleep_for(5s); - + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, agent, instance](boost::system::error_code ec) { @@ -1176,22 +1176,22 @@ Port = 0 EXPECT_TRUE(sink); const auto rest = dynamic_pointer_cast(sink); EXPECT_TRUE(rest); - + EXPECT_NE(agent, agent2); EXPECT_NE(instance, rest->instanceId()); } }); m_config->stop(); }); - + m_config->start(); th.join(); } - + TEST_F(ConfigTest, should_reload_device_xml_and_add_new_devices) { fs::path root {createTempDirectory("4")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1204,29 +1204,29 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("min_config.xml", devices, 1min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &context = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto chg = printer->getModelChangeTime(); auto device = agent->getDeviceByName("LinuxCNC"); - + auto dataItem = device->getDeviceDataItem("c1"); ASSERT_TRUE(dataItem); ASSERT_EQ("SPINDLE_SPEED", dataItem->getType()); - + DataItemPtr di; - + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices](boost::system::error_code ec) { @@ -1240,7 +1240,7 @@ Port = 0 fs::copy_options::overwrite_existing); } }); - + boost::asio::steady_timer timer2(context.get()); timer2.expires_from_now(6s); timer2.async_wait([this](boost::system::error_code ec) { @@ -1249,20 +1249,20 @@ Port = 0 auto agent = m_config->getAgent(); auto devices = agent->getDevices(); EXPECT_EQ(3, devices.size()); - + auto last = devices.back(); EXPECT_TRUE(last); EXPECT_EQ("001", last->getUuid()); - + const auto &dis = last->getDeviceDataItems(); EXPECT_EQ(5, dis.size()); - + EXPECT_TRUE(last->getDeviceDataItem("xd1")); EXPECT_TRUE(last->getDeviceDataItem("xex")); EXPECT_TRUE(last->getDeviceDataItem("o1_asset_chg")); EXPECT_TRUE(last->getDeviceDataItem("o1_asset_rem")); EXPECT_TRUE(last->getDeviceDataItem("o1_asset_count")); - + EXPECT_TRUE(agent->getDataItemById("xd1")); EXPECT_TRUE(agent->getDataItemById("xex")); EXPECT_TRUE(agent->getDataItemById("o1_asset_rem")); @@ -1271,44 +1271,44 @@ Port = 0 } m_config->stop(); }); - + m_config->start(); } - + TEST_F(ConfigTest, should_disable_agent_device) { string streams("SchemaVersion = 2.0\nDisableAgentDevice = true\n"); - + m_config->loadConfig(streams); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); - + auto devices = agent->getDevices(); ASSERT_EQ(1, devices.size()); - + auto device = devices.front(); ASSERT_EQ("Device", device->getName()); } - + TEST_F(ConfigTest, should_default_not_disable_agent_device) { string streams("SchemaVersion = 2.0\n"); - + m_config->loadConfig(streams); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); - + auto devices = agent->getDevices(); ASSERT_EQ(2, devices.size()); - + auto device = devices.front(); ASSERT_EQ("Agent", device->getName()); } - + TEST_F(ConfigTest, should_update_schema_version_when_device_file_updates) { auto root {createTempDirectory("5")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1321,29 +1321,29 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("min_config.xml", devices, 10min); replaceTextInFile(devices, "2.0", "1.2"); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto agent = m_config->getAgent(); auto &context = m_config->getAsyncContext(); auto sink = agent->findSink("RestService"); auto rest = dynamic_pointer_cast(sink); ASSERT_TRUE(rest); - + auto instance = rest->instanceId(); sink.reset(); rest.reset(); - + const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); ASSERT_EQ("1.2", *printer->getSchemaVersion()); - + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, &devices, agent](boost::system::error_code ec) { @@ -1357,16 +1357,16 @@ Port = 0 EXPECT_TRUE(di); EXPECT_EQ("SPINDLE_SPEED", di->getType()); di.reset(); - + // Modify devices replaceTextInFile(devices, "SPINDLE_SPEED", "ROTARY_VELOCITY"); replaceTextInFile(devices, "1.2", "1.3"); } }); - + auto th = thread([this, agent, instance, &context]() { this_thread::sleep_for(5s); - + boost::asio::steady_timer timer1(context.get()); timer1.expires_from_now(1s); timer1.async_wait([this, agent, instance](boost::system::error_code ec) { @@ -1377,33 +1377,33 @@ Port = 0 EXPECT_TRUE(sink); const auto rest = dynamic_pointer_cast(sink); EXPECT_TRUE(rest); - + EXPECT_NE(agent, agent2); EXPECT_NE(instance, rest->instanceId()); - + auto dataItem = agent2->getDataItemById("c1"); EXPECT_TRUE(dataItem); EXPECT_EQ("ROTARY_VELOCITY", dataItem->getType()); - + const auto &printer = agent2->getPrinter("xml"); EXPECT_NE(nullptr, printer); ASSERT_EQ("1.3", *printer->getSchemaVersion()); } }); - + m_config->stop(); }); - + m_config->start(); th.join(); } - + TEST_F(ConfigTest, should_add_a_new_device_when_deviceModel_received_from_adapter) { using namespace mtconnect::source::adapter; - + fs::path root {createTempDirectory("6")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1420,31 +1420,31 @@ Adapters { )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("empty.xml", devices, 0min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &asyncContext = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto sp = agent->findSource("_localhost_7878"); ASSERT_TRUE(sp); - + auto adapter = dynamic_pointer_cast(sp); ASSERT_TRUE(adapter); - + auto validate = [&](boost::system::error_code ec) { using namespace std::filesystem; using namespace std::chrono; using namespace boost::algorithm; - + if (!ec) { // Check for backup file @@ -1454,27 +1454,27 @@ Adapters { copy_if(dit, end(dit), back_inserter(entries), [&ext](const auto &de) { return contains(de.path().string(), ext); }); ASSERT_EQ(1, entries.size()); - + auto device = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device) << "Cannot find LinuxCNC device"; - + const auto &components = device->getChildren(); ASSERT_EQ(1, components->size()); - + auto cont = device->getComponentById("cont"); ASSERT_TRUE(cont) << "Cannot find Component with id cont"; - + auto exec = device->getDeviceDataItem("exec"); ASSERT_TRUE(exec) << "Cannot find DataItem with id exec"; - + auto pipeline = dynamic_cast(adapter->getPipeline()); ASSERT_EQ("LinuxCNC", pipeline->getDevice()); } m_config->stop(); }; - + boost::asio::steady_timer timer2(asyncContext.get()); - + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { if (ec) { @@ -1500,25 +1500,25 @@ Adapters { )"); adapter->processData("--multiline--AAAAA"); - + timer2.expires_from_now(500ms); timer2.async_wait(validate); } }; - + boost::asio::steady_timer timer1(asyncContext.get()); timer1.expires_from_now(100ms); timer1.async_wait(send); - + m_config->start(); } - + TEST_F(ConfigTest, should_update_a_device_when_received_from_adapter) { using namespace mtconnect::source::adapter; - + fs::path root {createTempDirectory("7")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1532,34 +1532,34 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("dyn_load.xml", devices, 0min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &asyncContext = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); auto device = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device); - + const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto sp = agent->findSource("_localhost_7878"); ASSERT_TRUE(sp); - + auto adapter = dynamic_pointer_cast(sp); ASSERT_TRUE(adapter); - + auto validate = [&](boost::system::error_code ec) { using namespace std::filesystem; using namespace std::chrono; using namespace boost::algorithm; - + if (!ec) { // Check for backup file @@ -1569,44 +1569,44 @@ Port = 0 copy_if(dit, end(dit), back_inserter(entries), [&ext](const auto &de) { return contains(de.path().string(), ext); }); ASSERT_EQ(2, entries.size()); - + auto device = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device) << "Cannot find LinuxCNC device"; - + const auto &components = device->getChildren(); ASSERT_EQ(1, components->size()); - + auto conts = device->getComponentByType("Controller"); ASSERT_EQ(1, conts.size()) << "Cannot find Component with id cont"; auto cont = conts.front(); - + auto devDIs = device->getDataItems(); ASSERT_TRUE(devDIs); ASSERT_EQ(5, devDIs->size()); - + auto dataItems = cont->getDataItems(); ASSERT_TRUE(dataItems); ASSERT_EQ(2, dataItems->size()); - + auto it = dataItems->begin(); ASSERT_EQ("exc", (*it)->get("originalId")); it++; ASSERT_EQ("mode", (*it)->get("originalId")); - + auto estop = device->getDeviceDataItem("estop"); ASSERT_TRUE(estop) << "Cannot find DataItem with id estop"; - + auto exec = device->getDeviceDataItem("exc"); ASSERT_TRUE(exec) << "Cannot find DataItem with id exc"; - + auto pipeline = dynamic_cast(adapter->getPipeline()); ASSERT_EQ("LinuxCNC", pipeline->getDevice()); } m_config->stop(); }; - + boost::asio::steady_timer timer2(asyncContext.get()); - + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { if (ec) { @@ -1633,23 +1633,23 @@ Port = 0 )"); adapter->processData("--multiline--AAAAA"); - + timer2.expires_from_now(500ms); timer2.async_wait(validate); } }; - + boost::asio::steady_timer timer1(asyncContext.get()); timer1.expires_from_now(100ms); timer1.async_wait(send); - + m_config->start(); } - + TEST_F(ConfigTest, should_update_the_ids_of_all_entities) { fs::path root {createTempDirectory("8")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1661,55 +1661,55 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("dyn_load.xml", devices, 0min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); - + auto agent = m_config->getAgent(); auto device = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device); - + auto deviceId = device->getId(); - + ASSERT_NE("d", deviceId); ASSERT_EQ("d", device->get("originalId")); - + // Get the data item by its old id auto exec = device->getDeviceDataItem("exec"); ASSERT_TRUE(exec); ASSERT_TRUE(exec->getOriginalId()); ASSERT_EQ("exec", *exec->getOriginalId()); - + // Re-initialize the agent with the modified device.xml with the unique ids aready created // This tests if the originalId in the device xml file does the ritght thing when mapping ids m_config = std::make_unique(); m_config->setDebug(true); m_config->initialize(options); - + auto agent2 = m_config->getAgent(); - + auto device2 = agent2->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device2); ASSERT_EQ(deviceId, device2->getId()); - + auto exec2 = device->getDeviceDataItem("exec"); ASSERT_TRUE(exec2); ASSERT_EQ(exec->getId(), exec2->getId()); ASSERT_TRUE(exec2->getOriginalId()); ASSERT_EQ("exec", *exec2->getOriginalId()); } - + TEST_F(ConfigTest, should_add_a_new_device_with_duplicate_ids) { using namespace mtconnect::source::adapter; - + fs::path root {createTempDirectory("9")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1723,34 +1723,34 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("dyn_load.xml", devices, 0min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &asyncContext = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); auto device = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device); - + const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto sp = agent->findSource("_localhost_7878"); ASSERT_TRUE(sp); - + auto adapter = dynamic_pointer_cast(sp); ASSERT_TRUE(adapter); - + auto validate = [&](boost::system::error_code ec) { using namespace std::filesystem; using namespace std::chrono; using namespace boost::algorithm; - + if (!ec) { // Check for backup file @@ -1760,23 +1760,23 @@ Port = 0 copy_if(dit, end(dit), back_inserter(entries), [&ext](const auto &de) { return contains(de.path().string(), ext); }); ASSERT_EQ(2, entries.size()); - + ASSERT_EQ(3, agent->getDevices().size()); - + auto device1 = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device1) << "Cannot find LinuxCNC device"; - + auto device2 = agent->getDeviceByName("AnotherCNC"); ASSERT_TRUE(device2) << "Cannot find LinuxCNC device"; - + auto pipeline = dynamic_cast(adapter->getPipeline()); ASSERT_EQ("AnotherCNC", pipeline->getDevice()); } m_config->stop(); }; - + boost::asio::steady_timer timer2(asyncContext.get()); - + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { if (ec) { @@ -1786,7 +1786,7 @@ Port = 0 { auto pipeline = dynamic_cast(adapter->getPipeline()); ASSERT_EQ("LinuxCNC", pipeline->getDevice()); - + adapter->processData("* deviceModel: --multiline--AAAAA"); adapter->processData(R"( @@ -1806,25 +1806,25 @@ Port = 0 )"); adapter->processData("--multiline--AAAAA"); - + timer2.expires_from_now(500ms); timer2.async_wait(validate); } }; - + boost::asio::steady_timer timer1(asyncContext.get()); timer1.expires_from_now(100ms); timer1.async_wait(send); - + m_config->start(); } - + TEST_F(ConfigTest, should_ignore_xmlns_when_parsing_device_xml) { using namespace mtconnect::source::adapter; - + fs::path root {createTempDirectory("10")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1841,31 +1841,31 @@ Adapters { )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("empty.xml", devices, 0min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &asyncContext = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto sp = agent->findSource("_localhost_7878"); ASSERT_TRUE(sp); - + auto adapter = dynamic_pointer_cast(sp); ASSERT_TRUE(adapter); - + auto validate = [&](boost::system::error_code ec) { using namespace std::filesystem; using namespace std::chrono; using namespace boost::algorithm; - + if (!ec) { // Check for backup file @@ -1875,18 +1875,18 @@ Adapters { copy_if(dit, end(dit), back_inserter(entries), [&ext](const auto &de) { return contains(de.path().string(), ext); }); ASSERT_EQ(1, entries.size()); - + auto device = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device) << "Cannot find LinuxCNC device"; - + ASSERT_FALSE(device->maybeGet("xmlns")); ASSERT_FALSE(device->maybeGet("xmlns:m")); } m_config->stop(); }; - + boost::asio::steady_timer timer2(asyncContext.get()); - + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { if (ec) { @@ -1912,25 +1912,25 @@ Adapters { )"); adapter->processData("--multiline--AAAAA"); - + timer2.expires_from_now(500ms); timer2.async_wait(validate); } }; - + boost::asio::steady_timer timer1(asyncContext.get()); timer1.expires_from_now(100ms); timer1.async_wait(send); - + m_config->start(); } - + TEST_F(ConfigTest, should_not_reload_when_monitor_files_is_on) { using namespace mtconnect::source::adapter; - + fs::path root {createTempDirectory("11")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -1946,41 +1946,41 @@ Port = 0 )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("dyn_load.xml", devices, 0min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &asyncContext = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); auto device = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device); - + const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto sp = agent->findSource("_localhost_7878"); ASSERT_TRUE(sp); - + auto adapter = dynamic_pointer_cast(sp); ASSERT_TRUE(adapter); - + boost::asio::steady_timer shutdownTimer(asyncContext.get()); - + auto shudown = [this](boost::system::error_code ec) { LOG(info) << "Shutting down the configuration"; m_config->stop(); }; - + auto validate = [&](boost::system::error_code ec) { using namespace std::filesystem; using namespace std::chrono; using namespace boost::algorithm; - + if (!ec) { // Check for backup file @@ -1990,25 +1990,25 @@ Port = 0 copy_if(dit, end(dit), back_inserter(entries), [&ext](const auto &de) { return contains(de.path().string(), ext); }); ASSERT_EQ(2, entries.size()); - + ASSERT_EQ(3, agent->getDevices().size()); - + auto device1 = agent->getDeviceByName("LinuxCNC"); ASSERT_TRUE(device1) << "Cannot find LinuxCNC device"; - + auto device2 = agent->getDeviceByName("AnotherCNC"); ASSERT_TRUE(device2) << "Cannot find LinuxCNC device"; - + auto pipeline = dynamic_cast(adapter->getPipeline()); ASSERT_EQ("AnotherCNC", pipeline->getDevice()); } - + shutdownTimer.expires_from_now(3s); shutdownTimer.async_wait(shudown); }; - + boost::asio::steady_timer timer2(asyncContext.get()); - + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { if (ec) { @@ -2018,7 +2018,7 @@ Port = 0 { auto pipeline = dynamic_cast(adapter->getPipeline()); ASSERT_EQ("LinuxCNC", pipeline->getDevice()); - + adapter->processData("* deviceModel: --multiline--AAAAA"); adapter->processData(R"( @@ -2038,25 +2038,25 @@ Port = 0 )"); adapter->processData("--multiline--AAAAA"); - + timer2.expires_from_now(500ms); timer2.async_wait(validate); } }; - + boost::asio::steady_timer timer1(asyncContext.get()); timer1.expires_from_now(100ms); timer1.async_wait(send); - + m_config->start(); } - + TEST_F(ConfigTest, should_not_crash_when_there_are_no_devices_and_receives_data) { using namespace mtconnect::source::adapter; - + fs::path root {createTempDirectory("12")}; - + fs::path devices(root / "Devices.xml"); fs::path config {root / "agent.cfg"}; { @@ -2073,39 +2073,39 @@ Adapters { )DOC"; cfg << "Devices = " << devices << endl; } - + copySampleFile("empty.xml", devices, 0min); - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); auto &asyncContext = m_config->getAsyncContext(); - + auto agent = m_config->getAgent(); const auto &printer = agent->getPrinter("xml"); ASSERT_NE(nullptr, printer); - + auto sp = agent->findSource("_localhost_7878"); ASSERT_TRUE(sp); - + auto adapter = dynamic_pointer_cast(sp); ASSERT_TRUE(adapter); - + auto validate = [&](boost::system::error_code ec) { using namespace std::filesystem; using namespace std::chrono; using namespace boost::algorithm; - + if (!ec) { } m_config->stop(); }; - + boost::asio::steady_timer timer2(asyncContext.get()); - + auto send = [this, &adapter, &timer2, validate](boost::system::error_code ec) { if (ec) { @@ -2115,105 +2115,105 @@ Adapters { { adapter->processData("* device: none"); adapter->processData("* uuid: 12345"); - + timer2.expires_from_now(500ms); timer2.async_wait(validate); } }; - + boost::asio::steady_timer timer1(asyncContext.get()); timer1.expires_from_now(100ms); timer1.async_wait(send); - + m_config->start(); } - + // Environment variable tests TEST_F(ConfigTest, should_expand_environment_variables) { putenv(strdup("CONFIG_TEST=TestValue")); - + string config(R"DOC( ServiceName=$CONFIG_TEST )DOC"); - + m_config->setDebug(true); m_config->loadConfig(config); - + const auto &options = m_config->getAgent()->getOptions(); ASSERT_EQ("TestValue", *GetOption(options, configuration::ServiceName)); } - + TEST_F(ConfigTest, should_expand_options) { putenv(strdup("CONFIG_TEST=ShouldNotMatch")); - + string config(R"DOC( TestVariable=TestValue ServiceName=$TestVariable )DOC"); - + m_config->setDebug(true); m_config->loadConfig(config); - + const auto &options = m_config->getAgent()->getOptions(); ASSERT_EQ("TestValue", *GetOption(options, configuration::ServiceName)); } - + // Environment variable tests TEST_F(ConfigTest, should_expand_with_prefix_and_suffix) { putenv(strdup("CONFIG_TEST=TestValue")); - + string config(R"DOC( ServiceName=/some/prefix/$CONFIG_TEST:suffix )DOC"); - + m_config->setDebug(true); m_config->loadConfig(config); - + const auto &options = m_config->getAgent()->getOptions(); ASSERT_EQ("/some/prefix/TestValue:suffix", *GetOption(options, configuration::ServiceName)); } - + TEST_F(ConfigTest, should_expand_with_prefix_and_suffix_with_curly) { putenv(strdup("CONFIG_TEST=TestValue")); - + string config(R"DOC( ServiceName="some_prefix_${CONFIG_TEST}_suffix" )DOC"); - + m_config->setDebug(true); m_config->loadConfig(config); - + const auto &options = m_config->getAgent()->getOptions(); ASSERT_EQ("some_prefix_TestValue_suffix", *GetOption(options, configuration::ServiceName)); } - + TEST_F(ConfigTest, should_find_device_file_in_config_path) { fs::path root {createTempDirectory("13")}; copySampleFile("empty.xml", root / "test.xml", 0min); chdir(m_cwd.string().c_str()); m_config->updateWorkingDirectory(); - + string config("ConfigPath=\"/junk/folder," + root.string() + "\"\n" "Devices=test.xml\n"); - + m_config->setDebug(true); m_config->loadConfig(config); - + ASSERT_TRUE(m_config->getAgent()); } - + TEST_F(ConfigTest, should_support_json_format) { using namespace std::chrono_literals; - + string str("{ \"Devices\": \"" TEST_RESOURCE_DIR "/samples/test_config.xml\"," R"DOC( @@ -2230,40 +2230,40 @@ ServiceName="some_prefix_${CONFIG_TEST}_suffix" } } )DOC"); - + m_config->loadConfig(str, AgentConfiguration::JSON); - + const auto agent = m_config->getAgent(); ASSERT_TRUE(agent); const auto source = agent->getSources().back(); const auto adapter = dynamic_pointer_cast(source); - + ASSERT_EQ(23, (int)adapter->getPort()); ASSERT_EQ(std::string("10.211.55.1"), adapter->getServer()); ASSERT_TRUE(IsOptionSet(adapter->getOptions(), configuration::FilterDuplicates)); ASSERT_TRUE(IsOptionSet(adapter->getOptions(), configuration::AutoAvailable)); ASSERT_TRUE(IsOptionSet(adapter->getOptions(), configuration::IgnoreTimestamps)); - + ASSERT_EQ(2000s, adapter->getLegacyTimeout()); - + // TODO: Need to link to device to the adapter. // ASSERT_TRUE(device->m_preserveUuid); } - + TEST_F(ConfigTest, should_set_agent_device_uuid) { string config(R"DOC( SchemaVersion=2.3 AgentDeviceUUID = SOME_UUID )DOC"); - + m_config->setDebug(true); m_config->loadConfig(config); - + const auto &ad = m_config->getAgent()->getAgentDevice(); ASSERT_EQ("SOME_UUID", *(ad->getUuid())); } - + TEST_F(ConfigTest, should_set_device_uuid_when_specified_in_adapter_config) { string config(R"DOC( @@ -2275,15 +2275,15 @@ Adapters { } } )DOC"); - + m_config->setDebug(true); m_config->loadConfig(config); - + auto dev = m_config->getAgent()->getDeviceByName("Simplest"); ASSERT_TRUE(dev); ASSERT_EQ("NEW-UUID", *(dev->getUuid())); } - + TEST_F(ConfigTest, should_set_default_device_uuid_when_specified_in_adapter_config) { string config(R"DOC( @@ -2295,28 +2295,28 @@ Adapters { } } )DOC"); - + m_config->setDebug(true); m_config->loadConfig(config); - + auto dev = m_config->getAgent()->getDeviceByName("Simplest"); ASSERT_TRUE(dev); ASSERT_EQ("NEW-UUID", *(dev->getUuid())); } - + TEST_F(ConfigTest, should_update_stylesheet_versions) { fs::path root {createTempDirectory("14")}; - + fs::path styleDir {root / "styles"}; fs::create_directory(styleDir); - + fs::path styles {styleDir / "styles.xsl"}; copyFile("styles/styles.xsl", styles, 0min); - + fs::path devices(root / "Devices.xml"); copySampleFile("empty.xml", devices, 0min); - + fs::path config {root / "agent.cfg"}; { ofstream cfg(config.string()); @@ -2334,19 +2334,19 @@ Files { DevicesStyle { Location = /styles/styles.xsl } )DOC"; } - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); - + ifstream file(styles); ASSERT_TRUE(file.is_open()); - + stringstream sf; sf << file.rdbuf(); - + ASSERT_EQ(R"DOC( )DOC", sf.str()); - + m_config->stop(); } - + TEST_F(ConfigTest, should_update_stylesheet_versions_with_path) { fs::path root {createTempDirectory("15")}; - + fs::path styleDir {root / "styles"}; fs::create_directory(styleDir); - + fs::path styles {styleDir / "styles.xsl"}; copyFile("styles/styles.xsl", styles, 0min); - + fs::path devices(root / "Devices.xml"); copySampleFile("empty.xml", devices, 0min); - + fs::path config {root / "agent.cfg"}; { ofstream cfg(config.string()); @@ -2398,19 +2398,19 @@ DevicesStyle { } )DOC"; } - + boost::program_options::variables_map options; boost::program_options::variable_value value(boost::optional(config.string()), false); options.insert(make_pair("config-file"s, value)); - + m_config->initialize(options); - + ifstream file(styles); ASSERT_TRUE(file.is_open()); - + stringstream sf; sf << file.rdbuf(); - + ASSERT_EQ(R"DOC( )DOC", sf.str()); - + m_config->stop(); } - + TEST_F(ConfigTest, should_set_sender_from_config) { string streams("Sender = MachineXXX\n"); - + m_config->loadConfig(streams); auto agent = const_cast(m_config->getAgent()); ASSERT_TRUE(agent); auto printer = dynamic_cast(agent->getPrinter("xml")); ASSERT_TRUE(printer); - + auto sender = printer->getSenderName(); ASSERT_EQ("MachineXXX"s, sender); - } + } } // namespace diff --git a/test_package/json_printer_probe_test.cpp b/test_package/json_printer_probe_test.cpp index 2a3d3539a..36b6e711e 100644 --- a/test_package/json_printer_probe_test.cpp +++ b/test_package/json_printer_probe_test.cpp @@ -96,7 +96,6 @@ TEST_F(JsonPrinterProbeTest, DeviceRootAndDescription) ASSERT_EQ(10, jdoc.at("/MTConnectDevices/Header/assetCount"_json_pointer).get()); ASSERT_EQ("MachineXXX", jdoc.at("/MTConnectDevices/Header/sender"_json_pointer).get()); - auto devices = jdoc.at("/MTConnectDevices/Devices"_json_pointer); ASSERT_EQ(2_S, devices.size()); diff --git a/test_package/mqtt_sink_2_test.cpp b/test_package/mqtt_sink_2_test.cpp index bbd0370bd..111748a74 100644 --- a/test_package/mqtt_sink_2_test.cpp +++ b/test_package/mqtt_sink_2_test.cpp @@ -395,20 +395,21 @@ TEST_F(MqttSink2Test, mqtt_sink_should_publish_agent_device) createServer(options); startServer(); ASSERT_NE(0, m_port); - + entity::JsonParser parser; - + DevicePtr ad; string agent_topic; - + auto handler = make_unique(); bool gotDevice = false; - handler->m_receive = [&gotDevice, &parser, &agent_topic, &ad ](std::shared_ptr client, - const std::string &topic, const std::string &payload) { + handler->m_receive = [&gotDevice, &parser, &agent_topic, &ad](std::shared_ptr client, + const std::string &topic, + const std::string &payload) { EXPECT_EQ(agent_topic, topic); gotDevice = true; }; - + createClient(options, std::move(handler)); ASSERT_TRUE(startClient()); createAgent(); @@ -416,10 +417,10 @@ TEST_F(MqttSink2Test, mqtt_sink_should_publish_agent_device) ad = m_agentTestHelper->m_agent->getAgentDevice(); agent_topic = "MTConnect/Probe/Agent_"s + *ad->getUuid(); m_client->subscribe(agent_topic); - + auto service = m_agentTestHelper->getMqtt2Service(); - + ASSERT_TRUE(waitFor(60s, [&service]() { return service->isConnected(); })); - + ASSERT_TRUE(waitFor(1s, [&gotDevice]() { return gotDevice; })); } From 5364514f7a80dced53fe294d8367319f6c2571cb Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sat, 9 Mar 2024 18:19:49 -0500 Subject: [PATCH 2/3] Removed dos trailing ^M --- src/mtconnect/config.hpp | 208 +- .../source/adapter/mqtt/mqtt_adapter.cpp | 538 +++--- .../source/adapter/shdr/shdr_adapter.cpp | 464 ++--- src/mtconnect/utilities.hpp | 1670 ++++++++--------- 4 files changed, 1440 insertions(+), 1440 deletions(-) diff --git a/src/mtconnect/config.hpp b/src/mtconnect/config.hpp index 89ba4fbe7..219d245d6 100644 --- a/src/mtconnect/config.hpp +++ b/src/mtconnect/config.hpp @@ -1,104 +1,104 @@ -#pragma once - -// -// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// @file config.hpp -/// @brief common includes and cross platform requirements - -// TODO: Remove when BOOST fixes its multiple defined symbol issue with phoenix placeholders -#define BOOST_PHOENIX_STL_TUPLE_H_ -#define BOOST_BIND_NO_PLACEHOLDERS - -#include - -#ifndef __STDC_LIMIT_MACROS -#define __STDC_LIMIT_MACROS 1 -#endif -#if _MSC_VER > 1500 -#include -#else -#endif -#ifndef UINT64_MAX -#define UINT64_MAX 0xFFFFFFFFFFFFFFFFull -#endif -#ifndef NOMINMAX -#define NOMINMAX 1 -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(_WIN32) || defined(_WIN64) -#ifndef _WINDOWS -#define _WINDOWS 1 -#endif -#define ISNAN(x) _isnan(x) -#if _MSC_VER < 1800 -#define NAN numeric_limits::quiet_NaN() -#endif -#if _MSC_VER >= 1900 -#define gets gets_s -#define timezone _timezone -#endif -typedef unsigned __int64 uint64_t; -#else -#define O_BINARY 0 -#define ISNAN(x) std::isnan(x) -#include -#include -#include -#include -#endif - -#ifdef _WINDOWS -#define AGENT_SYMBOL_EXPORT __declspec(dllexport) -#define AGENT_SYMBOL_IMPORT __declspec(dllimport) -#else // _WINDOWS -#define AGENT_SYMBOL_EXPORT __attribute__((visibility("default"))) -#define AGENT_SYMBOL_IMPORT __attribute__((visibility("default"))) -#endif // _WINDOWS - -#ifdef SHARED_AGENT_LIB - -#ifdef AGENT_BUILD_SHARED_LIB -#define AGENT_LIB_API AGENT_SYMBOL_EXPORT -#else -#define AGENT_LIB_API AGENT_SYMBOL_IMPORT -#endif - -#define AGENT_SYMBOL_VISIBLE AGENT_LIB_API - -#else // SHARED_AGENT_LIB - -#define AGENT_LIB_API -#define AGENT_SYMBOL_VISIBLE - -#endif +#pragma once + +// +// Copyright Copyright 2009-2022, AMT � The Association For Manufacturing Technology (�AMT�) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/// @file config.hpp +/// @brief common includes and cross platform requirements + +// TODO: Remove when BOOST fixes its multiple defined symbol issue with phoenix placeholders +#define BOOST_PHOENIX_STL_TUPLE_H_ +#define BOOST_BIND_NO_PLACEHOLDERS + +#include + +#ifndef __STDC_LIMIT_MACROS +#define __STDC_LIMIT_MACROS 1 +#endif +#if _MSC_VER > 1500 +#include +#else +#endif +#ifndef UINT64_MAX +#define UINT64_MAX 0xFFFFFFFFFFFFFFFFull +#endif +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) +#ifndef _WINDOWS +#define _WINDOWS 1 +#endif +#define ISNAN(x) _isnan(x) +#if _MSC_VER < 1800 +#define NAN numeric_limits::quiet_NaN() +#endif +#if _MSC_VER >= 1900 +#define gets gets_s +#define timezone _timezone +#endif +typedef unsigned __int64 uint64_t; +#else +#define O_BINARY 0 +#define ISNAN(x) std::isnan(x) +#include +#include +#include +#include +#endif + +#ifdef _WINDOWS +#define AGENT_SYMBOL_EXPORT __declspec(dllexport) +#define AGENT_SYMBOL_IMPORT __declspec(dllimport) +#else // _WINDOWS +#define AGENT_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define AGENT_SYMBOL_IMPORT __attribute__((visibility("default"))) +#endif // _WINDOWS + +#ifdef SHARED_AGENT_LIB + +#ifdef AGENT_BUILD_SHARED_LIB +#define AGENT_LIB_API AGENT_SYMBOL_EXPORT +#else +#define AGENT_LIB_API AGENT_SYMBOL_IMPORT +#endif + +#define AGENT_SYMBOL_VISIBLE AGENT_LIB_API + +#else // SHARED_AGENT_LIB + +#define AGENT_LIB_API +#define AGENT_SYMBOL_VISIBLE + +#endif diff --git a/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp b/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp index 09e5915c7..de1399a03 100644 --- a/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp +++ b/src/mtconnect/source/adapter/mqtt/mqtt_adapter.cpp @@ -1,269 +1,269 @@ -// -// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#include "mqtt_adapter.hpp" - -#include -#include - -#include -#include - -#include "mtconnect/configuration/config_options.hpp" -#include "mtconnect/device_model/device.hpp" -#include "mtconnect/mqtt/mqtt_client_impl.hpp" -#include "mtconnect/pipeline/convert_sample.hpp" -#include "mtconnect/pipeline/deliver.hpp" -#include "mtconnect/pipeline/delta_filter.hpp" -#include "mtconnect/pipeline/duplicate_filter.hpp" -#include "mtconnect/pipeline/json_mapper.hpp" -#include "mtconnect/pipeline/message_mapper.hpp" -#include "mtconnect/pipeline/period_filter.hpp" -#include "mtconnect/pipeline/shdr_token_mapper.hpp" -#include "mtconnect/pipeline/shdr_tokenizer.hpp" -#include "mtconnect/pipeline/timestamp_extractor.hpp" -#include "mtconnect/pipeline/topic_mapper.hpp" -#include "mtconnect/pipeline/upcase_value.hpp" - -using namespace std; -namespace asio = boost::asio; - -namespace mtconnect { - using namespace observation; - using namespace entity; - using namespace pipeline; - using namespace source::adapter; - - namespace source::adapter::mqtt_adapter { - - MqttAdapter::MqttAdapter(boost::asio::io_context &io, - pipeline::PipelineContextPtr pipelineContext, - const ConfigOptions &options, const boost::property_tree::ptree &block) - : Adapter("MQTT", io, options), - m_ioContext(io), - m_strand(Source::m_strand), - m_pipeline(pipelineContext, Source::m_strand) - { - GetOptions(block, m_options, options); - AddOptions(block, m_options, - {{configuration::UUID, string()}, - {configuration::Manufacturer, string()}, - {configuration::Station, string()}, - {configuration::Url, string()}, - {configuration::MqttCaCert, string()}, - {configuration::MqttPrivateKey, string()}, - {configuration::MqttCert, string()}, - {configuration::MqttUserName, string()}, - {configuration::MqttPassword, string()}, - {configuration::MqttClientId, string()}, - {configuration::MqttHost, string()}}); - - AddDefaultedOptions(block, m_options, - {{configuration::MqttTls, false}, - {configuration::MqttWs, false}, - {configuration::AutoAvailable, false}, - {configuration::RealTime, false}, - {configuration::RelativeTime, false}}); - loadTopics(block, m_options); - - if (!HasOption(m_options, configuration::MqttHost) && - HasOption(m_options, configuration::Host)) - { - m_options[configuration::MqttHost] = m_options[configuration::Host]; - } - if (!HasOption(m_options, configuration::MqttPort) && - HasOption(m_options, configuration::Port)) - { - m_options[configuration::MqttPort] = m_options[configuration::Port]; - } - else - { - m_options[configuration::MqttPort] = 1883; - } - - m_handler = m_pipeline.makeHandler(); - auto clientHandler = make_unique(); - - m_pipeline.m_handler = m_handler.get(); - - clientHandler->m_connecting = [this](shared_ptr client) { - m_handler->m_connecting(client->getIdentity()); - }; - - clientHandler->m_connected = [this](shared_ptr client) { - client->connectComplete(); - m_handler->m_connected(client->getIdentity()); - subscribeToTopics(); - }; - - clientHandler->m_disconnected = [this](shared_ptr client) { - m_handler->m_disconnected(client->getIdentity()); - }; - - clientHandler->m_receive = [this](shared_ptr client, const std::string &topic, - const std::string &payload) { - m_handler->m_processMessage(topic, payload, client->getIdentity()); - }; - - if (IsOptionSet(m_options, configuration::MqttTls) && - !IsOptionSet(m_options, configuration::MqttWs)) - { - m_client = make_shared(m_ioContext, m_options, - move(clientHandler)); - } - else if (IsOptionSet(m_options, configuration::MqttWs) && - IsOptionSet(m_options, configuration::MqttTls)) - { - m_client = make_shared(m_ioContext, m_options, - std::move(clientHandler)); - } - else if (IsOptionSet(m_options, configuration::MqttWs)) - { - m_client = make_shared(m_ioContext, m_options, - std::move(clientHandler)); - } - else - { - m_client = make_shared(m_ioContext, m_options, - move(clientHandler)); - } - - m_identity = m_client->getIdentity(); - m_name = m_client->getUrl(); - - m_options[configuration::AdapterIdentity] = m_name; - m_pipeline.build(m_options); - } - - void MqttAdapter::loadTopics(const boost::property_tree::ptree &tree, ConfigOptions &options) - { - auto topics = tree.get_child_optional(configuration::Topics); - if (topics) - { - StringList list; - if (topics->size() == 0) - { - boost::split(list, topics->get_value(), boost::is_any_of(":"), - boost::token_compress_on); - } - else - { - for (auto &f : *topics) - { - list.emplace_back(f.second.data()); - } - } - options[configuration::Topics] = list; - } - else - { - LOG(error) << "MQTT Adapter requires at least one topic to subscribe to. Provide 'Topics = " - "' or Topics block"; - exit(1); - } - } - /// - /// - /// - /// - void MqttAdapter::registerFactory(SourceFactory &factory) - { - factory.registerFactory("mqtt", - [](const std::string &name, boost::asio::io_context &io, - pipeline::PipelineContextPtr context, const ConfigOptions &options, - const boost::property_tree::ptree &block) -> source::SourcePtr { - auto source = - std::make_shared(io, context, options, block); - return source; - }); - } - - const std::string &MqttAdapter::getHost() const { return m_host; } - - unsigned int MqttAdapter::getPort() const { return m_port; } - - bool MqttAdapter::start() - { - m_pipeline.start(); - return m_client->start(); - } - void MqttAdapter::stop() - { - m_client->stop(); - m_pipeline.clear(); - } - - void MqttAdapter::subscribeToTopics() - { - // If we have topics, subscribe - auto topics = GetOption(m_options, configuration::Topics); - LOG(info) << "MqttClientImpl::connect: subscribing to topics"; - if (topics) - { - for (const auto &topic : *topics) - { - m_client->subscribe(topic); - } - } - } - - mtconnect::pipeline::Pipeline *MqttAdapter::getPipeline() { return &m_pipeline; } - - void MqttPipeline::build(const ConfigOptions &options) - { - AdapterPipeline::build(options); - - buildDeviceList(); - buildCommandAndStatusDelivery(); - - // Build topic mapper pipeline - auto next = bind(make_shared( - m_context, GetOption(m_options, configuration::Device).value_or(""))); - - auto map1 = next->bind(make_shared(m_context)); - auto map2 = next->bind(make_shared(m_context, m_handler)); - - // SHDR Parsing Branch, if Data is sent down... - auto tokenizer = map2->bind(make_shared()); - auto shdr = tokenizer; - - auto extract = - make_shared(IsOptionSet(m_options, configuration::RelativeTime)); - shdr = shdr->bind(extract); - - // Token mapping to data items and asset - auto mapper = make_shared( - m_context, m_device.value_or(""), - GetOption(m_options, configuration::ShdrVersion).value_or(1)); - mapper->bind(make_shared(TypeGuard(RUN))); - shdr->bind(mapper); - - // Merge the pipelines - auto merge = - make_shared(TypeGuard(RUN) || TypeGuard(RUN)); - - mapper->bind(merge); - map1->bind(merge); - map2->bind(merge); - - buildAssetDelivery(merge); - buildObservationDelivery(merge); - applySplices(); - } - - } // namespace source::adapter::mqtt_adapter -} // namespace mtconnect +// +// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "mqtt_adapter.hpp" + +#include +#include + +#include +#include + +#include "mtconnect/configuration/config_options.hpp" +#include "mtconnect/device_model/device.hpp" +#include "mtconnect/mqtt/mqtt_client_impl.hpp" +#include "mtconnect/pipeline/convert_sample.hpp" +#include "mtconnect/pipeline/deliver.hpp" +#include "mtconnect/pipeline/delta_filter.hpp" +#include "mtconnect/pipeline/duplicate_filter.hpp" +#include "mtconnect/pipeline/json_mapper.hpp" +#include "mtconnect/pipeline/message_mapper.hpp" +#include "mtconnect/pipeline/period_filter.hpp" +#include "mtconnect/pipeline/shdr_token_mapper.hpp" +#include "mtconnect/pipeline/shdr_tokenizer.hpp" +#include "mtconnect/pipeline/timestamp_extractor.hpp" +#include "mtconnect/pipeline/topic_mapper.hpp" +#include "mtconnect/pipeline/upcase_value.hpp" + +using namespace std; +namespace asio = boost::asio; + +namespace mtconnect { + using namespace observation; + using namespace entity; + using namespace pipeline; + using namespace source::adapter; + + namespace source::adapter::mqtt_adapter { + + MqttAdapter::MqttAdapter(boost::asio::io_context &io, + pipeline::PipelineContextPtr pipelineContext, + const ConfigOptions &options, const boost::property_tree::ptree &block) + : Adapter("MQTT", io, options), + m_ioContext(io), + m_strand(Source::m_strand), + m_pipeline(pipelineContext, Source::m_strand) + { + GetOptions(block, m_options, options); + AddOptions(block, m_options, + {{configuration::UUID, string()}, + {configuration::Manufacturer, string()}, + {configuration::Station, string()}, + {configuration::Url, string()}, + {configuration::MqttCaCert, string()}, + {configuration::MqttPrivateKey, string()}, + {configuration::MqttCert, string()}, + {configuration::MqttUserName, string()}, + {configuration::MqttPassword, string()}, + {configuration::MqttClientId, string()}, + {configuration::MqttHost, string()}}); + + AddDefaultedOptions(block, m_options, + {{configuration::MqttTls, false}, + {configuration::MqttWs, false}, + {configuration::AutoAvailable, false}, + {configuration::RealTime, false}, + {configuration::RelativeTime, false}}); + loadTopics(block, m_options); + + if (!HasOption(m_options, configuration::MqttHost) && + HasOption(m_options, configuration::Host)) + { + m_options[configuration::MqttHost] = m_options[configuration::Host]; + } + if (!HasOption(m_options, configuration::MqttPort) && + HasOption(m_options, configuration::Port)) + { + m_options[configuration::MqttPort] = m_options[configuration::Port]; + } + else + { + m_options[configuration::MqttPort] = 1883; + } + + m_handler = m_pipeline.makeHandler(); + auto clientHandler = make_unique(); + + m_pipeline.m_handler = m_handler.get(); + + clientHandler->m_connecting = [this](shared_ptr client) { + m_handler->m_connecting(client->getIdentity()); + }; + + clientHandler->m_connected = [this](shared_ptr client) { + client->connectComplete(); + m_handler->m_connected(client->getIdentity()); + subscribeToTopics(); + }; + + clientHandler->m_disconnected = [this](shared_ptr client) { + m_handler->m_disconnected(client->getIdentity()); + }; + + clientHandler->m_receive = [this](shared_ptr client, const std::string &topic, + const std::string &payload) { + m_handler->m_processMessage(topic, payload, client->getIdentity()); + }; + + if (IsOptionSet(m_options, configuration::MqttTls) && + !IsOptionSet(m_options, configuration::MqttWs)) + { + m_client = make_shared(m_ioContext, m_options, + move(clientHandler)); + } + else if (IsOptionSet(m_options, configuration::MqttWs) && + IsOptionSet(m_options, configuration::MqttTls)) + { + m_client = make_shared(m_ioContext, m_options, + std::move(clientHandler)); + } + else if (IsOptionSet(m_options, configuration::MqttWs)) + { + m_client = make_shared(m_ioContext, m_options, + std::move(clientHandler)); + } + else + { + m_client = make_shared(m_ioContext, m_options, + move(clientHandler)); + } + + m_identity = m_client->getIdentity(); + m_name = m_client->getUrl(); + + m_options[configuration::AdapterIdentity] = m_name; + m_pipeline.build(m_options); + } + + void MqttAdapter::loadTopics(const boost::property_tree::ptree &tree, ConfigOptions &options) + { + auto topics = tree.get_child_optional(configuration::Topics); + if (topics) + { + StringList list; + if (topics->size() == 0) + { + boost::split(list, topics->get_value(), boost::is_any_of(":"), + boost::token_compress_on); + } + else + { + for (auto &f : *topics) + { + list.emplace_back(f.second.data()); + } + } + options[configuration::Topics] = list; + } + else + { + LOG(error) << "MQTT Adapter requires at least one topic to subscribe to. Provide 'Topics = " + "' or Topics block"; + exit(1); + } + } + /// + /// + /// + /// + void MqttAdapter::registerFactory(SourceFactory &factory) + { + factory.registerFactory("mqtt", + [](const std::string &name, boost::asio::io_context &io, + pipeline::PipelineContextPtr context, const ConfigOptions &options, + const boost::property_tree::ptree &block) -> source::SourcePtr { + auto source = + std::make_shared(io, context, options, block); + return source; + }); + } + + const std::string &MqttAdapter::getHost() const { return m_host; } + + unsigned int MqttAdapter::getPort() const { return m_port; } + + bool MqttAdapter::start() + { + m_pipeline.start(); + return m_client->start(); + } + void MqttAdapter::stop() + { + m_client->stop(); + m_pipeline.clear(); + } + + void MqttAdapter::subscribeToTopics() + { + // If we have topics, subscribe + auto topics = GetOption(m_options, configuration::Topics); + LOG(info) << "MqttClientImpl::connect: subscribing to topics"; + if (topics) + { + for (const auto &topic : *topics) + { + m_client->subscribe(topic); + } + } + } + + mtconnect::pipeline::Pipeline *MqttAdapter::getPipeline() { return &m_pipeline; } + + void MqttPipeline::build(const ConfigOptions &options) + { + AdapterPipeline::build(options); + + buildDeviceList(); + buildCommandAndStatusDelivery(); + + // Build topic mapper pipeline + auto next = bind(make_shared( + m_context, GetOption(m_options, configuration::Device).value_or(""))); + + auto map1 = next->bind(make_shared(m_context)); + auto map2 = next->bind(make_shared(m_context, m_handler)); + + // SHDR Parsing Branch, if Data is sent down... + auto tokenizer = map2->bind(make_shared()); + auto shdr = tokenizer; + + auto extract = + make_shared(IsOptionSet(m_options, configuration::RelativeTime)); + shdr = shdr->bind(extract); + + // Token mapping to data items and asset + auto mapper = make_shared( + m_context, m_device.value_or(""), + GetOption(m_options, configuration::ShdrVersion).value_or(1)); + mapper->bind(make_shared(TypeGuard(RUN))); + shdr->bind(mapper); + + // Merge the pipelines + auto merge = + make_shared(TypeGuard(RUN) || TypeGuard(RUN)); + + mapper->bind(merge); + map1->bind(merge); + map2->bind(merge); + + buildAssetDelivery(merge); + buildObservationDelivery(merge); + applySplices(); + } + + } // namespace source::adapter::mqtt_adapter +} // namespace mtconnect diff --git a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp index 21c4ea5ce..38a25d0fe 100644 --- a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp +++ b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp @@ -1,232 +1,232 @@ -// -// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#define __STDC_LIMIT_MACROS 1 -#include "shdr_adapter.hpp" - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "mtconnect/configuration/config_options.hpp" -#include "mtconnect/device_model/device.hpp" -#include "mtconnect/logging.hpp" - -using namespace std; -using namespace std::literals; -using namespace date::literals; - -namespace mtconnect::source::adapter::shdr { - // Adapter public methods - ShdrAdapter::ShdrAdapter(boost::asio::io_context &io, - pipeline::PipelineContextPtr pipelineContext, - const ConfigOptions &options, const boost::property_tree::ptree &block) - : Adapter("ShdrAdapter", io, options), - Connector(Source::m_strand, "", 0, 60s), - m_pipeline(pipelineContext, Source::m_strand), - m_running(true) - { - GetOptions(block, m_options, options); - AddOptions(block, m_options, - {{configuration::Heartbeat, Milliseconds {0}}, - {configuration::UUID, string()}, - {configuration::Manufacturer, string()}, - {configuration::AdapterIdentity, string()}, - {configuration::Station, string()}, - {configuration::Url, string()}}); - - m_options.erase(configuration::Host); - m_options.erase(configuration::Port); - m_heartbeatOverride = GetOption(m_options, configuration::Heartbeat); - - AddDefaultedOptions(block, m_options, - {{configuration::Host, "localhost"s}, - {configuration::Port, 7878}, - {configuration::AutoAvailable, false}, - {configuration::RealTime, false}, - {configuration::RelativeTime, false}, - {configuration::SuppressIPAddress, false}, - {configuration::EnableSourceDeviceModels, false}}); - - m_server = get(m_options[configuration::Host]); - m_port = get(m_options[configuration::Port]); - - auto timeout = m_options.find(configuration::LegacyTimeout); - if (timeout != m_options.end()) - m_legacyTimeout = get(timeout->second); - - stringstream url; - url << "shdr://" << m_server << ':' << m_port; - m_name = url.str(); - - stringstream identity; - identity << '_' << m_server << '_' << m_port; - - if (auto ident = GetOption(m_options, configuration::AdapterIdentity)) - { - m_identity = *ident; - } - else - { - if (IsOptionSet(m_options, configuration::SuppressIPAddress)) - { - boost::uuids::detail::sha1 sha1; - sha1.process_bytes(identity.str().c_str(), identity.str().length()); - boost::uuids::detail::sha1::digest_type digest; - sha1.get_digest(digest); - - identity.str(""); - identity << std::hex << digest[0] << digest[1] << digest[2]; - m_identity = string("_") + (identity.str()).substr(0, 10); - } - else - { - m_identity = identity.str(); - } - m_options[configuration::AdapterIdentity] = m_identity; - } - - m_handler = m_pipeline.makeHandler(); - if (m_pipeline.hasContract()) - m_pipeline.build(m_options); - auto intv = GetOption(options, configuration::ReconnectInterval); - if (intv) - m_reconnectInterval = *intv; - - if (m_reconnectInterval < 500ms) - { - LOG(warning) << "Reconnection interval set to " << m_reconnectInterval.count() - << "ms, limiting it to 500ms"; - m_reconnectInterval = 500ms; - } - } - - void ShdrAdapter::processData(const string &data) - { - NAMED_SCOPE("ShdrAdapter::processData"); - - try - { - if (m_terminator) - { - if (data == *m_terminator) - { - forwardData(m_body.str()); - m_terminator.reset(); - m_body.str(""); - } - else - { - m_body << std::endl << data; - } - } - else if (size_t multi = data.find("--multiline--"); multi != std::string::npos) - { - m_body.str(""); - m_body << data.substr(0, multi); - m_terminator = data.substr(multi); - } - else - { - forwardData(data); - } - } - catch (std::exception &e) - { - LOG(error) << "Error in processData: " << e.what(); - } - catch (...) - { - LOG(error) << "Unknown exception in processData"; - } - } - - void ShdrAdapter::stop() - { - NAMED_SCOPE("ShdrAdapter::stop"); - // Will stop threaded object gracefully Adapter::thread() - LOG(debug) << "Waiting for adapter to stop: " << m_name; - m_running = false; - close(); - - m_pipeline.clear(); - LOG(debug) << "Adapter exited: " << m_name; - } - - inline bool is_true(const std::string &value) { return value == "yes" || value == "true"; } - - void ShdrAdapter::protocolCommand(const std::string &data) - { - NAMED_SCOPE("ShdrAdapter::protocolCommand"); - - using namespace boost::algorithm; - namespace qi = boost::spirit::qi; - namespace ascii = boost::spirit::ascii; - namespace phoenix = boost::phoenix; - - using ascii::space; - using qi::char_; - using qi::lexeme; - using qi::lit; - - string command; - auto f = [&command](const auto &s) { command = string(s.begin(), s.end()); }; - - auto it = data.begin(); - bool res = - qi::phrase_parse(it, data.end(), (lit("*") >> lexeme[+(char_ - ':')][f] >> ':'), space); - - if (res) - { - string value(it, data.end()); - ConfigOptions options; - - boost::to_lower(command); - - if (command == "conversionrequired") - options[configuration::ConversionRequired] = is_true(value); - else if (command == "relativetime") - options[configuration::RelativeTime] = is_true(value); - else if (command == "realtime") - options[configuration::RealTime] = is_true(value); - else if (command == "device") - options[configuration::Device] = value; - else if (command == "shdrversion") - options[configuration::ShdrVersion] = stringToInt(value, 1); - else if (command == "messsage") - { - LOG(info) << '[' << getIdentity() << "] Adapter message: " << value; - return; - } - - if (options.size() > 0) - setOptions(options); - else if (m_handler && m_handler->m_command) - m_handler->m_command(command, value, getIdentity()); - } - else - { - LOG(warning) << "protocolCommand: Cannot parse command: " << data; - } - } -} // namespace mtconnect::source::adapter::shdr +// +// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#define __STDC_LIMIT_MACROS 1 +#include "shdr_adapter.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "mtconnect/configuration/config_options.hpp" +#include "mtconnect/device_model/device.hpp" +#include "mtconnect/logging.hpp" + +using namespace std; +using namespace std::literals; +using namespace date::literals; + +namespace mtconnect::source::adapter::shdr { + // Adapter public methods + ShdrAdapter::ShdrAdapter(boost::asio::io_context &io, + pipeline::PipelineContextPtr pipelineContext, + const ConfigOptions &options, const boost::property_tree::ptree &block) + : Adapter("ShdrAdapter", io, options), + Connector(Source::m_strand, "", 0, 60s), + m_pipeline(pipelineContext, Source::m_strand), + m_running(true) + { + GetOptions(block, m_options, options); + AddOptions(block, m_options, + {{configuration::Heartbeat, Milliseconds {0}}, + {configuration::UUID, string()}, + {configuration::Manufacturer, string()}, + {configuration::AdapterIdentity, string()}, + {configuration::Station, string()}, + {configuration::Url, string()}}); + + m_options.erase(configuration::Host); + m_options.erase(configuration::Port); + m_heartbeatOverride = GetOption(m_options, configuration::Heartbeat); + + AddDefaultedOptions(block, m_options, + {{configuration::Host, "localhost"s}, + {configuration::Port, 7878}, + {configuration::AutoAvailable, false}, + {configuration::RealTime, false}, + {configuration::RelativeTime, false}, + {configuration::SuppressIPAddress, false}, + {configuration::EnableSourceDeviceModels, false}}); + + m_server = get(m_options[configuration::Host]); + m_port = get(m_options[configuration::Port]); + + auto timeout = m_options.find(configuration::LegacyTimeout); + if (timeout != m_options.end()) + m_legacyTimeout = get(timeout->second); + + stringstream url; + url << "shdr://" << m_server << ':' << m_port; + m_name = url.str(); + + stringstream identity; + identity << '_' << m_server << '_' << m_port; + + if (auto ident = GetOption(m_options, configuration::AdapterIdentity)) + { + m_identity = *ident; + } + else + { + if (IsOptionSet(m_options, configuration::SuppressIPAddress)) + { + boost::uuids::detail::sha1 sha1; + sha1.process_bytes(identity.str().c_str(), identity.str().length()); + boost::uuids::detail::sha1::digest_type digest; + sha1.get_digest(digest); + + identity.str(""); + identity << std::hex << digest[0] << digest[1] << digest[2]; + m_identity = string("_") + (identity.str()).substr(0, 10); + } + else + { + m_identity = identity.str(); + } + m_options[configuration::AdapterIdentity] = m_identity; + } + + m_handler = m_pipeline.makeHandler(); + if (m_pipeline.hasContract()) + m_pipeline.build(m_options); + auto intv = GetOption(options, configuration::ReconnectInterval); + if (intv) + m_reconnectInterval = *intv; + + if (m_reconnectInterval < 500ms) + { + LOG(warning) << "Reconnection interval set to " << m_reconnectInterval.count() + << "ms, limiting it to 500ms"; + m_reconnectInterval = 500ms; + } + } + + void ShdrAdapter::processData(const string &data) + { + NAMED_SCOPE("ShdrAdapter::processData"); + + try + { + if (m_terminator) + { + if (data == *m_terminator) + { + forwardData(m_body.str()); + m_terminator.reset(); + m_body.str(""); + } + else + { + m_body << std::endl << data; + } + } + else if (size_t multi = data.find("--multiline--"); multi != std::string::npos) + { + m_body.str(""); + m_body << data.substr(0, multi); + m_terminator = data.substr(multi); + } + else + { + forwardData(data); + } + } + catch (std::exception &e) + { + LOG(error) << "Error in processData: " << e.what(); + } + catch (...) + { + LOG(error) << "Unknown exception in processData"; + } + } + + void ShdrAdapter::stop() + { + NAMED_SCOPE("ShdrAdapter::stop"); + // Will stop threaded object gracefully Adapter::thread() + LOG(debug) << "Waiting for adapter to stop: " << m_name; + m_running = false; + close(); + + m_pipeline.clear(); + LOG(debug) << "Adapter exited: " << m_name; + } + + inline bool is_true(const std::string &value) { return value == "yes" || value == "true"; } + + void ShdrAdapter::protocolCommand(const std::string &data) + { + NAMED_SCOPE("ShdrAdapter::protocolCommand"); + + using namespace boost::algorithm; + namespace qi = boost::spirit::qi; + namespace ascii = boost::spirit::ascii; + namespace phoenix = boost::phoenix; + + using ascii::space; + using qi::char_; + using qi::lexeme; + using qi::lit; + + string command; + auto f = [&command](const auto &s) { command = string(s.begin(), s.end()); }; + + auto it = data.begin(); + bool res = + qi::phrase_parse(it, data.end(), (lit("*") >> lexeme[+(char_ - ':')][f] >> ':'), space); + + if (res) + { + string value(it, data.end()); + ConfigOptions options; + + boost::to_lower(command); + + if (command == "conversionrequired") + options[configuration::ConversionRequired] = is_true(value); + else if (command == "relativetime") + options[configuration::RelativeTime] = is_true(value); + else if (command == "realtime") + options[configuration::RealTime] = is_true(value); + else if (command == "device") + options[configuration::Device] = value; + else if (command == "shdrversion") + options[configuration::ShdrVersion] = stringToInt(value, 1); + else if (command == "messsage") + { + LOG(info) << '[' << getIdentity() << "] Adapter message: " << value; + return; + } + + if (options.size() > 0) + setOptions(options); + else if (m_handler && m_handler->m_command) + m_handler->m_command(command, value, getIdentity()); + } + else + { + LOG(warning) << "protocolCommand: Cannot parse command: " << data; + } + } +} // namespace mtconnect::source::adapter::shdr diff --git a/src/mtconnect/utilities.hpp b/src/mtconnect/utilities.hpp index 3d7d4a502..8f668b443 100644 --- a/src/mtconnect/utilities.hpp +++ b/src/mtconnect/utilities.hpp @@ -1,835 +1,835 @@ -// -// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) -// All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// @file utilities.hpp -/// @brief Common utility functions - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "mtconnect/config.hpp" -#include "mtconnect/logging.hpp" - -// ####### CONSTANTS ####### - -// Port number to put server on -const unsigned int SERVER_PORT = 8080; - -// Size of sliding buffer -const unsigned int DEFAULT_SLIDING_BUFFER_SIZE = 131072; - -// Size of buffer exponent: 2^SLIDING_BUFFER_EXP -const unsigned int DEFAULT_SLIDING_BUFFER_EXP = 17; -const unsigned int DEFAULT_MAX_ASSETS = 1024; - -namespace boost::asio { - class io_context; -} - -/// @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; - - /// @brief Time formats - enum TimeFormat - { - HUM_READ, ///< Human readable - GMT, ///< GMT or UTC with second resolution - GMT_UV_SEC, ///< GMT with microsecond resolution - LOCAL ///< Time using local time zone - }; - - /// @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; - try - { - value = stof(text); - } - catch (const std::out_of_range &) - { - value = 0.0; - } - catch (const std::invalid_argument &) - { - value = 0.0; - } - 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; - try - { - value = stoi(text); - } - catch (const std::out_of_range &) - { - value = outOfRangeDefault; - } - catch (const std::invalid_argument &) - { - value = 0; - } - return value; - } - - /// @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; - constexpr int precision = std::numeric_limits::digits10; - s << std::setprecision(precision) << value; - 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) - { - constexpr int precision = std::numeric_limits::digits10; - os << std::setprecision(precision) << fmter.val; - return os; - } - }; - - /// @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); } - - /// @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(), - [](unsigned char c) { return std::toupper(c); }); - - return text; - } - - /// @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) - { - if (!isdigit(c)) - return false; - } - - 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(); - if (*iter == '-' || *iter == '+') - ++iter; - - for (; iter != s.end(); iter++) - { - if (!isdigit(*iter)) - return false; - } - - 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); - - /// @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) - { - using namespace std; - using namespace std::chrono; - constexpr char ISO_8601_FMT[] = "%Y-%m-%dT%H:%M:%SZ"; - - switch (format) - { - case HUM_READ: - return date::format("%a, %d %b %Y %H:%M:%S GMT", date::floor(timePoint)); - case GMT: - return date::format(ISO_8601_FMT, date::floor(timePoint)); - case GMT_UV_SEC: - return date::format(ISO_8601_FMT, date::floor(timePoint)); - case LOCAL: - auto time = system_clock::to_time_t(timePoint); - struct tm timeinfo = {0}; - mt_localtime(&time, &timeinfo); - char timestamp[64] = {0}; - strftime(timestamp, 50u, "%Y-%m-%dT%H:%M:%S%z", &timeinfo); - return timestamp; - } - - return ""; - } - - /// @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() - { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - } - - /// @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 - inline uint64_t parseTimeMicro(const std::string &aTime) - { - std::stringstream str(aTime); - if (isdigit(aTime.back())) - { - str.seekp(0, std::ios_base::end); - str << 'Z'; - str.seekg(0); - } - using micros = std::chrono::time_point; - date::fields fields; - std::chrono::minutes offset; - std::string abbrev; - date::from_stream(str, "%FT%T%Z", fields, &abbrev, &offset); - if (!fields.ymd.ok() || !fields.tod.in_conventional_range()) - return 0; - - micros microdays {date::sys_days(fields.ymd)}; - auto us = fields.tod.to_duration().count() + microdays.time_since_epoch().count(); - return us; - } - - /// @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++) - { - char c = data[i]; - - switch (c) - { - case '&': - data.replace(i, 1, "&"); - break; - - case '<': - data.replace(i, 1, "<"); - break; - - case '>': - data.replace(i, 1, ">"); - break; - } - } - } - - /// @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); - - /// @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()) - return false; - 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; - } - - /// @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; - } - - /// @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 split a string into two parts using a ':' separator - /// @param key the key to split - /// @return a pair of the key and an optional prefix. - static inline std::pair> splitKey(const std::string &key) - { - auto c = key.find(':'); - if (c != std::string::npos) - return {key.substr(c + 1, std::string::npos), key.substr(0, c)}; - else - return {key, std::nullopt}; - } - - /// @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()) - return false; - 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()) - return false; - - return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char a, char b) { - return tolower(a) == tolower(b); - }); - } - - using Attributes = std::map; - - /// @brief overloaded pattern for variant visitors using list of lambdas - /// @tparam ...Ts list of lambda classes - template - struct overloaded : Ts... - { - using Ts::operator()...; - }; - template - overloaded(Ts...) -> overloaded; - - /// @brief Reverse an iterable - /// @tparam T The iterable type - template - class reverse - { - private: - T &m_iterable; - - public: - explicit reverse(T &iterable) : m_iterable(iterable) {} - auto begin() const { return std::rbegin(m_iterable); } - 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; - using Microseconds = std::chrono::microseconds; - using Seconds = std::chrono::seconds; - using Timestamp = std::chrono::time_point; - using Timestamp = std::chrono::time_point; - using StringList = std::list; - - /// @name Configuration related methods - ///@{ - - /// @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) - { - auto v = options.find(name); - if (v != options.end()) - return std::get(v->second); - else - 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); - if (v != options.end()) - return std::get(v->second); - else - 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, - const ConfigOptions &options) - { - ConfigOption option {s}; - if (std::holds_alternative(option)) - { - std::string sv = std::get(option); - visit(overloaded {[&option, &sv](const std::string &) { - if (sv.empty()) - option = std::monostate(); - else - option = sv; - }, - [&option, &sv](const int &) { option = stoi(sv); }, - [&option, &sv](const Milliseconds &) { option = Milliseconds {stoi(sv)}; }, - [&option, &sv](const Seconds &) { option = Seconds {stoi(sv)}; }, - [&option, &sv](const double &) { option = stod(sv); }, - [&option, &sv](const bool &) { option = sv == "yes" || sv == "true"; }, - [&option, &sv](const StringList &) { - StringList list; - boost::split(list, sv, boost::is_any_of(",")); - for (auto &s : list) - boost::trim(s); - option = list; - }, - [](const auto &) {}}, - def); - } - 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) - { - using namespace std; - using boost::regex; - using boost::smatch; - - auto value = GetOption(options, name); - if (value) - { - static const regex pat("([0-9]+)([GgMmKkBb]*)"); - smatch match; - string v = *value; - if (regex_match(v, match, pat)) - { - size = boost::lexical_cast(match[1]); - if (match[2].matched) - { - switch (match[2].str()[0]) - { - case 'G': - case 'g': - size *= 1024; - - case 'M': - case 'm': - size *= 1024; - - case 'K': - case 'k': - size *= 1024; - } - } - } - else - { - std::stringstream msg; - msg << "Invalid value for " << name << ": " << *value << endl; - throw std::runtime_error(msg.str()); - } - } - - 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) - { - for (auto &e : entries) - { - auto val = tree.get_optional(e.first); - if (val) - { - auto v = ConvertOption(*val, e.second, options); - if (v.index() != 0) - options.insert_or_assign(e.first, v); - } - } - } - - /// @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) - { - for (auto &e : entries) - { - auto val = tree.get_optional(e.first); - if (val) - { - auto v = ConvertOption(*val, e.second, options); - if (v.index() != 0) - options.insert_or_assign(e.first, v); - } - else if (options.find(e.first) == options.end()) - options.insert_or_assign(e.first, e.second); - } - } - - /// @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) - { - options.insert_or_assign(e.first, e.second); - } - } - - /// @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) - { - for (auto &e : entries) - { - if (!std::holds_alternative(e.second) || - !std::get(e.second).empty()) - { - options.emplace(e.first, e.second); - } - } - 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; - string time = date::format("%FT%T", date::floor(ts)); - auto pos = time.find_last_not_of("0"); - if (pos != string::npos) - { - if (time[pos] != '.') - pos++; - time.erase(pos); - } - time.append("Z"); - 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; - - // Exceptions to the rule - const static std::unordered_map exceptions = { - {"AC", "AC"}, {"DC", "DC"}, {"PH", "PH"}, - {"IP", "IP"}, {"URI", "URI"}, {"MTCONNECT", "MTConnect"}}; - - const auto &w = exceptions.find(std::string(start, end)); - if (w != exceptions.end()) - { - copy(w->second.begin(), w->second.end(), start); - } - else - { - *start = ::toupper(*start); - start++; - transform(start, end, start, ::tolower); - } - } - - /// @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; - if (type.empty()) - return ""; - - string camel; - auto colon = type.find(':'); - - if (colon != string::npos) - { - prefix = type.substr(0ul, colon); - camel = type.substr(colon + 1ul); - } - else - camel = type; - - auto start = camel.begin(); - decltype(start) end; - - bool done; - do - { - end = find(start, camel.end(), '_'); - capitalize(start, end); - done = end == camel.end(); - if (!done) - { - camel.erase(end); - start = end; - } - } while (!done); - - 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; - using namespace std::chrono; - using namespace std::chrono_literals; - using namespace date::literals; - - Timestamp ts; - std::istringstream in(timestamp); - in >> std::setw(6) >> parse("%FT%T", ts); - if (!in.good()) - { - ts = std::chrono::system_clock::now(); - } - 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); - } - - inline constexpr int32_t IntDefaultSchemaVersion() - { - 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 {0}, minor {0}; - char c; - std::stringstream vstr(s); - vstr >> major >> c >> minor; - if (major == 0) - { - return IntDefaultSchemaVersion(); - } - else - { - return SCHEMA_VERSION(major, minor); - } - } - - /// @brief Retrieve the best Host IP address from the network interfaces. - /// @param[in] context the boost asio io_context for resolving the address - /// @param[in] onlyV4 only consider IPV4 addresses if `true` - std::string GetBestHostAddress(boost::asio::io_context &context, bool onlyV4 = false); - - /// @brief Function to create a unique id given a sha1 namespace and an id. - /// - /// Creates a base 64 encoded version of the string and removes any illegal characters - /// for an ID. If the first character is not a legal start character, maps the first 2 characters - /// to the legal ID start char set. - /// - /// @param[in] sha the sha1 namespace to use as context - /// @param[in] id the id to use transform - /// @returns Returns the first 16 characters of the base 64 encoded sha1 - inline std::string makeUniqueId(const boost::uuids::detail::sha1 &sha, const std::string &id) - { - using namespace std; - - boost::uuids::detail::sha1 sha1(sha); - - constexpr string_view startc("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"); - constexpr auto isIDStartChar = [](unsigned char c) -> bool { return isalpha(c) || c == '_'; }; - constexpr auto isIDChar = [isIDStartChar](unsigned char c) -> bool { - return isIDStartChar(c) || isdigit(c) || c == '.' || c == '-'; - }; - - sha1.process_bytes(id.data(), id.length()); - unsigned int digest[5]; - sha1.get_digest(digest); - - string s(32, ' '); - auto len = boost::beast::detail::base64::encode(s.data(), digest, sizeof(digest)); - - s.erase(len - 1); - s.erase(std::remove_if(++(s.begin()), s.end(), not_fn(isIDChar)), s.end()); - - // Check if the character is legal. - if (!isIDStartChar(s[0])) - { - // Change the start character to a legal character - uint32_t c = s[0] + s[1]; - s.erase(0, 1); - s[0] = startc[c % startc.size()]; - } - - s.erase(16); - - return s; - } -} // namespace mtconnect +// +// Copyright Copyright 2009-2022, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/// @file utilities.hpp +/// @brief Common utility functions + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "mtconnect/config.hpp" +#include "mtconnect/logging.hpp" + +// ####### CONSTANTS ####### + +// Port number to put server on +const unsigned int SERVER_PORT = 8080; + +// Size of sliding buffer +const unsigned int DEFAULT_SLIDING_BUFFER_SIZE = 131072; + +// Size of buffer exponent: 2^SLIDING_BUFFER_EXP +const unsigned int DEFAULT_SLIDING_BUFFER_EXP = 17; +const unsigned int DEFAULT_MAX_ASSETS = 1024; + +namespace boost::asio { + class io_context; +} + +/// @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; + + /// @brief Time formats + enum TimeFormat + { + HUM_READ, ///< Human readable + GMT, ///< GMT or UTC with second resolution + GMT_UV_SEC, ///< GMT with microsecond resolution + LOCAL ///< Time using local time zone + }; + + /// @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; + try + { + value = stof(text); + } + catch (const std::out_of_range &) + { + value = 0.0; + } + catch (const std::invalid_argument &) + { + value = 0.0; + } + 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; + try + { + value = stoi(text); + } + catch (const std::out_of_range &) + { + value = outOfRangeDefault; + } + catch (const std::invalid_argument &) + { + value = 0; + } + return value; + } + + /// @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; + constexpr int precision = std::numeric_limits::digits10; + s << std::setprecision(precision) << value; + 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) + { + constexpr int precision = std::numeric_limits::digits10; + os << std::setprecision(precision) << fmter.val; + return os; + } + }; + + /// @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); } + + /// @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(), + [](unsigned char c) { return std::toupper(c); }); + + return text; + } + + /// @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) + { + if (!isdigit(c)) + return false; + } + + 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(); + if (*iter == '-' || *iter == '+') + ++iter; + + for (; iter != s.end(); iter++) + { + if (!isdigit(*iter)) + return false; + } + + 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); + + /// @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) + { + using namespace std; + using namespace std::chrono; + constexpr char ISO_8601_FMT[] = "%Y-%m-%dT%H:%M:%SZ"; + + switch (format) + { + case HUM_READ: + return date::format("%a, %d %b %Y %H:%M:%S GMT", date::floor(timePoint)); + case GMT: + return date::format(ISO_8601_FMT, date::floor(timePoint)); + case GMT_UV_SEC: + return date::format(ISO_8601_FMT, date::floor(timePoint)); + case LOCAL: + auto time = system_clock::to_time_t(timePoint); + struct tm timeinfo = {0}; + mt_localtime(&time, &timeinfo); + char timestamp[64] = {0}; + strftime(timestamp, 50u, "%Y-%m-%dT%H:%M:%S%z", &timeinfo); + return timestamp; + } + + return ""; + } + + /// @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() + { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + } + + /// @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 + inline uint64_t parseTimeMicro(const std::string &aTime) + { + std::stringstream str(aTime); + if (isdigit(aTime.back())) + { + str.seekp(0, std::ios_base::end); + str << 'Z'; + str.seekg(0); + } + using micros = std::chrono::time_point; + date::fields fields; + std::chrono::minutes offset; + std::string abbrev; + date::from_stream(str, "%FT%T%Z", fields, &abbrev, &offset); + if (!fields.ymd.ok() || !fields.tod.in_conventional_range()) + return 0; + + micros microdays {date::sys_days(fields.ymd)}; + auto us = fields.tod.to_duration().count() + microdays.time_since_epoch().count(); + return us; + } + + /// @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++) + { + char c = data[i]; + + switch (c) + { + case '&': + data.replace(i, 1, "&"); + break; + + case '<': + data.replace(i, 1, "<"); + break; + + case '>': + data.replace(i, 1, ">"); + break; + } + } + } + + /// @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); + + /// @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()) + return false; + 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; + } + + /// @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; + } + + /// @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 split a string into two parts using a ':' separator + /// @param key the key to split + /// @return a pair of the key and an optional prefix. + static inline std::pair> splitKey(const std::string &key) + { + auto c = key.find(':'); + if (c != std::string::npos) + return {key.substr(c + 1, std::string::npos), key.substr(0, c)}; + else + return {key, std::nullopt}; + } + + /// @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()) + return false; + 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()) + return false; + + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char a, char b) { + return tolower(a) == tolower(b); + }); + } + + using Attributes = std::map; + + /// @brief overloaded pattern for variant visitors using list of lambdas + /// @tparam ...Ts list of lambda classes + template + struct overloaded : Ts... + { + using Ts::operator()...; + }; + template + overloaded(Ts...) -> overloaded; + + /// @brief Reverse an iterable + /// @tparam T The iterable type + template + class reverse + { + private: + T &m_iterable; + + public: + explicit reverse(T &iterable) : m_iterable(iterable) {} + auto begin() const { return std::rbegin(m_iterable); } + 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; + using Microseconds = std::chrono::microseconds; + using Seconds = std::chrono::seconds; + using Timestamp = std::chrono::time_point; + using Timestamp = std::chrono::time_point; + using StringList = std::list; + + /// @name Configuration related methods + ///@{ + + /// @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) + { + auto v = options.find(name); + if (v != options.end()) + return std::get(v->second); + else + 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); + if (v != options.end()) + return std::get(v->second); + else + 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, + const ConfigOptions &options) + { + ConfigOption option {s}; + if (std::holds_alternative(option)) + { + std::string sv = std::get(option); + visit(overloaded {[&option, &sv](const std::string &) { + if (sv.empty()) + option = std::monostate(); + else + option = sv; + }, + [&option, &sv](const int &) { option = stoi(sv); }, + [&option, &sv](const Milliseconds &) { option = Milliseconds {stoi(sv)}; }, + [&option, &sv](const Seconds &) { option = Seconds {stoi(sv)}; }, + [&option, &sv](const double &) { option = stod(sv); }, + [&option, &sv](const bool &) { option = sv == "yes" || sv == "true"; }, + [&option, &sv](const StringList &) { + StringList list; + boost::split(list, sv, boost::is_any_of(",")); + for (auto &s : list) + boost::trim(s); + option = list; + }, + [](const auto &) {}}, + def); + } + 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) + { + using namespace std; + using boost::regex; + using boost::smatch; + + auto value = GetOption(options, name); + if (value) + { + static const regex pat("([0-9]+)([GgMmKkBb]*)"); + smatch match; + string v = *value; + if (regex_match(v, match, pat)) + { + size = boost::lexical_cast(match[1]); + if (match[2].matched) + { + switch (match[2].str()[0]) + { + case 'G': + case 'g': + size *= 1024; + + case 'M': + case 'm': + size *= 1024; + + case 'K': + case 'k': + size *= 1024; + } + } + } + else + { + std::stringstream msg; + msg << "Invalid value for " << name << ": " << *value << endl; + throw std::runtime_error(msg.str()); + } + } + + 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) + { + for (auto &e : entries) + { + auto val = tree.get_optional(e.first); + if (val) + { + auto v = ConvertOption(*val, e.second, options); + if (v.index() != 0) + options.insert_or_assign(e.first, v); + } + } + } + + /// @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) + { + for (auto &e : entries) + { + auto val = tree.get_optional(e.first); + if (val) + { + auto v = ConvertOption(*val, e.second, options); + if (v.index() != 0) + options.insert_or_assign(e.first, v); + } + else if (options.find(e.first) == options.end()) + options.insert_or_assign(e.first, e.second); + } + } + + /// @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) + { + options.insert_or_assign(e.first, e.second); + } + } + + /// @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) + { + for (auto &e : entries) + { + if (!std::holds_alternative(e.second) || + !std::get(e.second).empty()) + { + options.emplace(e.first, e.second); + } + } + 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; + string time = date::format("%FT%T", date::floor(ts)); + auto pos = time.find_last_not_of("0"); + if (pos != string::npos) + { + if (time[pos] != '.') + pos++; + time.erase(pos); + } + time.append("Z"); + 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; + + // Exceptions to the rule + const static std::unordered_map exceptions = { + {"AC", "AC"}, {"DC", "DC"}, {"PH", "PH"}, + {"IP", "IP"}, {"URI", "URI"}, {"MTCONNECT", "MTConnect"}}; + + const auto &w = exceptions.find(std::string(start, end)); + if (w != exceptions.end()) + { + copy(w->second.begin(), w->second.end(), start); + } + else + { + *start = ::toupper(*start); + start++; + transform(start, end, start, ::tolower); + } + } + + /// @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; + if (type.empty()) + return ""; + + string camel; + auto colon = type.find(':'); + + if (colon != string::npos) + { + prefix = type.substr(0ul, colon); + camel = type.substr(colon + 1ul); + } + else + camel = type; + + auto start = camel.begin(); + decltype(start) end; + + bool done; + do + { + end = find(start, camel.end(), '_'); + capitalize(start, end); + done = end == camel.end(); + if (!done) + { + camel.erase(end); + start = end; + } + } while (!done); + + 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; + using namespace std::chrono; + using namespace std::chrono_literals; + using namespace date::literals; + + Timestamp ts; + std::istringstream in(timestamp); + in >> std::setw(6) >> parse("%FT%T", ts); + if (!in.good()) + { + ts = std::chrono::system_clock::now(); + } + 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); + } + + inline constexpr int32_t IntDefaultSchemaVersion() + { + 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 {0}, minor {0}; + char c; + std::stringstream vstr(s); + vstr >> major >> c >> minor; + if (major == 0) + { + return IntDefaultSchemaVersion(); + } + else + { + return SCHEMA_VERSION(major, minor); + } + } + + /// @brief Retrieve the best Host IP address from the network interfaces. + /// @param[in] context the boost asio io_context for resolving the address + /// @param[in] onlyV4 only consider IPV4 addresses if `true` + std::string GetBestHostAddress(boost::asio::io_context &context, bool onlyV4 = false); + + /// @brief Function to create a unique id given a sha1 namespace and an id. + /// + /// Creates a base 64 encoded version of the string and removes any illegal characters + /// for an ID. If the first character is not a legal start character, maps the first 2 characters + /// to the legal ID start char set. + /// + /// @param[in] sha the sha1 namespace to use as context + /// @param[in] id the id to use transform + /// @returns Returns the first 16 characters of the base 64 encoded sha1 + inline std::string makeUniqueId(const boost::uuids::detail::sha1 &sha, const std::string &id) + { + using namespace std; + + boost::uuids::detail::sha1 sha1(sha); + + constexpr string_view startc("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"); + constexpr auto isIDStartChar = [](unsigned char c) -> bool { return isalpha(c) || c == '_'; }; + constexpr auto isIDChar = [isIDStartChar](unsigned char c) -> bool { + return isIDStartChar(c) || isdigit(c) || c == '.' || c == '-'; + }; + + sha1.process_bytes(id.data(), id.length()); + unsigned int digest[5]; + sha1.get_digest(digest); + + string s(32, ' '); + auto len = boost::beast::detail::base64::encode(s.data(), digest, sizeof(digest)); + + s.erase(len - 1); + s.erase(std::remove_if(++(s.begin()), s.end(), not_fn(isIDChar)), s.end()); + + // Check if the character is legal. + if (!isIDStartChar(s[0])) + { + // Change the start character to a legal character + uint32_t c = s[0] + s[1]; + s.erase(0, 1); + s[0] = startc[c % startc.size()]; + } + + s.erase(16); + + return s; + } +} // namespace mtconnect From b2a0fc31f6d13d06a1d3aef404f20455928b8525 Mon Sep 17 00:00:00 2001 From: Will Sobel Date: Sat, 9 Mar 2024 19:06:28 -0500 Subject: [PATCH 3/3] Fixed multiple definition of ws client --- src/mtconnect/mqtt/mqtt_client_impl.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mtconnect/mqtt/mqtt_client_impl.hpp b/src/mtconnect/mqtt/mqtt_client_impl.hpp index d29abae60..dc1747d4e 100644 --- a/src/mtconnect/mqtt/mqtt_client_impl.hpp +++ b/src/mtconnect/mqtt/mqtt_client_impl.hpp @@ -51,8 +51,6 @@ namespace mtconnect { using mqtt_client_ws_ptr = decltype(mqtt::make_async_client_ws(std::declval()...)); template using mqtt_tls_client_ws_ptr = decltype(mqtt::make_tls_async_client_ws(std::declval()...)); - template - using mqtt_client_ws_ptr = decltype(mqtt::make_async_client_ws(std::declval()...)); using mqtt_client = mqtt_client_ptr;