diff --git a/CMakeLists.txt b/CMakeLists.txt index 758a427e..2630f01f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 5) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 1) +set(AGENT_VERSION_BUILD 2) set(AGENT_VERSION_RC "") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile index 6740e358..270e90ae 100644 --- a/docker/alpine/Dockerfile +++ b/docker/alpine/Dockerfile @@ -44,8 +44,8 @@ ARG CONAN_CPU_COUNT=2 ARG WITH_RUBY='True' # set some variables -ENV PATH="$HOME/venv3.9/bin:$PATH" -ENV CONAN_PROFILE='/root/agent/cppagent/conan/profiles/docker' +ENV HOME='/root' +ENV CONAN_PROFILE="$HOME/agent/cppagent/conan/profiles/docker" # update os and add dependencies # note: Dockerfiles run as root by default, so don't need sudo @@ -90,7 +90,7 @@ RUN if [ -z "$WITH_TESTS" ] || [ "$WITH_TESTS" = "false" ]; then \ -o agent_prefix=mtc \ -o cpack=True \ -o "with_ruby=$WITH_RUBY" \ - -o cpack_destination=/root/agent \ + -o "cpack_destination=$HOME/agent" \ -o cpack_name=dist \ -o cpack_generator=TGZ \ -pr "$CONAN_PROFILE" \ diff --git a/src/mtconnect/device_model/configuration/coordinate_systems.cpp b/src/mtconnect/device_model/configuration/coordinate_systems.cpp index adc9be6f..a36b2789 100644 --- a/src/mtconnect/device_model/configuration/coordinate_systems.cpp +++ b/src/mtconnect/device_model/configuration/coordinate_systems.cpp @@ -30,7 +30,9 @@ namespace mtconnect { { auto transformation = make_shared( Requirements {Requirement("Translation", ValueType::VECTOR, 3, false), - Requirement("Rotation", ValueType::VECTOR, 3, false)}); + Requirement("Rotation", ValueType::VECTOR, 3, false), + Requirement("TranslationDataSet", ValueType::DATA_SET, false), + Requirement("RotationDataSet", ValueType::DATA_SET, false)}); auto coordinateSystem = make_shared(Requirements { Requirement("id", true), Requirement("name", false), Requirement("nativeName", false), @@ -40,8 +42,10 @@ namespace mtconnect { ControlledVocab {"WORLD", "BASE", "OBJECT", "TASK", "MECHANICAL_INTERFACE", "TOOL", "MOBILE_PLATFORM", "MACHINE", "CAMERA"}, true), - Requirement("Origin", ValueType::VECTOR, 3, false), + Requirement("Description", false), Requirement("Origin", ValueType::VECTOR, 3, false), + Requirement("OriginDataSet", ValueType::DATA_SET, false), Requirement("Transformation", ValueType::ENTITY, transformation, false)}); + coordinateSystem->setOrder({"Description", "Origin", "OriginDataSet", "Transformation"}); coordinateSystems = make_shared(Requirements {Requirement( "CoordinateSystem", ValueType::ENTITY, coordinateSystem, 1, Requirement::Infinite)}); diff --git a/src/mtconnect/device_model/configuration/motion.cpp b/src/mtconnect/device_model/configuration/motion.cpp index 5a863a44..1d7d06c3 100644 --- a/src/mtconnect/device_model/configuration/motion.cpp +++ b/src/mtconnect/device_model/configuration/motion.cpp @@ -27,7 +27,9 @@ namespace mtconnect { { static auto transformation = make_shared( Requirements {Requirement("Translation", ValueType::VECTOR, 3, false), - Requirement("Rotation", ValueType::VECTOR, 3, false)}); + Requirement("Rotation", ValueType::VECTOR, 3, false), + Requirement("TranslationDataSet", ValueType::DATA_SET, false), + Requirement("RotationDataSet", ValueType::DATA_SET, false)}); static auto motion = make_shared(Requirements { Requirement("id", true), Requirement("parentIdRef", false), @@ -35,9 +37,13 @@ namespace mtconnect { Requirement("type", ControlledVocab {"REVOLUTE", "CONTINUOUS", "PRISMATIC", "FIXED"}, true), Requirement("actuation", ControlledVocab {"DIRECT", "VIRTUAL", "NONE"}, true), - Requirement("Description", false), Requirement("Axis", ValueType::VECTOR, 3, true), + Requirement("Description", false), Requirement("Axis", ValueType::VECTOR, 3, false), + Requirement("AxisDataSet", ValueType::DATA_SET, false), Requirement("Origin", ValueType::VECTOR, 3, false), + Requirement("OriginDataSet", ValueType::DATA_SET, false), Requirement("Transformation", ValueType::ENTITY, transformation, false)}); + motion->setOrder( + {"Description", "Axis", "AxisDataSet", "Origin", "OriginDataSet", "Transformation"}); return motion; } diff --git a/src/mtconnect/entity/factory.hpp b/src/mtconnect/entity/factory.hpp index b5e1d411..6da238d4 100644 --- a/src/mtconnect/entity/factory.hpp +++ b/src/mtconnect/entity/factory.hpp @@ -138,6 +138,16 @@ namespace mtconnect { return m_simpleProperties.count(name) > 0; } + /// @brief is this requirement resolved with a data set + /// @param name the name of the property + /// @return `true` if this is a data set + bool isDataSet(const std::string &name) const { return m_dataSets.count(name) > 0; } + + /// @brief is this requirement resolved with a table + /// @param name the name of the property + /// @return `true` if this is a table + bool isTable(const std::string &name) const { return m_tables.count(name) > 0; } + /// @brief get the requirement pointer for a key /// @param name the property key /// @return requirement pointer @@ -378,6 +388,12 @@ namespace mtconnect { { m_hasRaw = true; } + else if (BaseValueType(r.getType()) == ValueType::DATA_SET) + { + m_dataSets.insert(r.getName()); + if (r.getType() == ValueType::TABLE) + m_tables.insert(r.getName()); + } else { m_simpleProperties.insert(r.getName()); @@ -429,6 +445,8 @@ namespace mtconnect { bool m_any {false}; std::set m_propertySets; + std::set m_dataSets; + std::set m_tables; std::set m_simpleProperties; std::set m_properties; }; diff --git a/src/mtconnect/entity/requirement.hpp b/src/mtconnect/entity/requirement.hpp index a4a9832b..1ed29f0b 100644 --- a/src/mtconnect/entity/requirement.hpp +++ b/src/mtconnect/entity/requirement.hpp @@ -76,7 +76,11 @@ namespace mtconnect::entity { }; /// @brief Mask for value types - const int16_t VALUE_TYPE_BASE = 0x0F; + const std::uint16_t VALUE_TYPE_BASE = 0x0F; + constexpr const ValueType BaseValueType(const ValueType value) + { + return ValueType(std::uint16_t(value) & VALUE_TYPE_BASE); + } class Factory; using FactoryPtr = std::shared_ptr; diff --git a/src/mtconnect/entity/xml_parser.cpp b/src/mtconnect/entity/xml_parser.cpp index 26d4b4cd..af787df7 100644 --- a/src/mtconnect/entity/xml_parser.cpp +++ b/src/mtconnect/entity/xml_parser.cpp @@ -17,6 +17,9 @@ #include "mtconnect/entity/xml_parser.hpp" +#include +#include + #include #include #include @@ -88,6 +91,81 @@ namespace mtconnect::entity { return content; } + template + static inline bool isType(const string &str, const P &parser, A &value) + { + std::string::const_iterator first(str.cbegin()), last(str.cend()); + return boost::spirit::qi::parse(first, last, parser, value) && first == last; + } + + static void parseDataSet(xmlNodePtr node, DataSet &dataSet, bool table, bool cell = false) + { + for (xmlNodePtr child = node->children; child; child = child->next) + { + if (child->type == XML_ELEMENT_NODE && + ((!cell && xmlStrcmp(child->name, BAD_CAST "Entry") == 0) || + (cell && xmlStrcmp(child->name, BAD_CAST "Cell") == 0))) + { + // Get the key for the entry + string key; + for (xmlAttrPtr attr = child->properties; attr; attr = attr->next) + { + if (attr->type == XML_ATTRIBUTE_NODE) + { + string name((const char *)attr->name); + if (name != "key") + { + throw EntityError("parseDataSet: Expecting ksy for data set Entry: " + + string((const char *)node->name)); + } + key = string((const char *)attr->children->content); + break; + } + } + + /// TODO: Add support for tables + DataSetValue value; + auto valueNode = child->children; + if (valueNode) + { + if (table && !cell) + { + DataSet &ds = value.emplace(); + parseDataSet(child, ds, true, true); + } + else if (valueNode->type == XML_TEXT_NODE) + { + string text = ((const char *)valueNode->content); + trim(text); + + if (int64_t v; isType(text, boost::spirit::long_long, v)) + { + value = v; + } + else if (double v; isType(text, boost::spirit::double_, v)) + { + value = v; + } + else + { + value = text; + } + } + else + { + LOG(warning) << "Invalid content for data set"; + } + } + dataSet.emplace(key, value); + } + else + { + throw EntityError("parseDataSet: Expecting Entry for data set: " + + string((const char *)node->name)); + } + } + } + EntityPtr XmlParser::parseXmlNode(FactoryPtr factory, xmlNodePtr node, ErrorList &errors, bool parseNamespaces) { @@ -165,7 +243,12 @@ namespace mtconnect::entity { } } - if (simple) + if (ef->isDataSet(name)) + { + auto ds = &properties[name].emplace(); + parseDataSet(child, *ds, ef->isTable(name)); + } + else if (simple) { if (child->children != nullptr && child->children->content != nullptr) { diff --git a/src/mtconnect/sink/rest_sink/server.cpp b/src/mtconnect/sink/rest_sink/server.cpp index e6de2f07..dd34add9 100644 --- a/src/mtconnect/sink/rest_sink/server.cpp +++ b/src/mtconnect/sink/rest_sink/server.cpp @@ -314,7 +314,7 @@ namespace mtconnect::sink::rest_sink { obj.AddPairs("description", "OK"); { AutoJsonObject obj(writer, "content"); - { + { AutoJsonObject obj(writer, "text/plain"); { AutoJsonObject obj(writer, "schema"); diff --git a/test_package/entity_parser_test.cpp b/test_package/entity_parser_test.cpp index ab752021..8128915f 100644 --- a/test_package/entity_parser_test.cpp +++ b/test_package/entity_parser_test.cpp @@ -317,3 +317,70 @@ TEST_F(EntityParserTest, check_proper_line_truncation) ASSERT_EQ("Description", entity->getName()); ASSERT_EQ("And some text", entity->getValue()); } + +TEST_F(EntityParserTest, should_parse_data_sets) +{ + auto ds = make_shared(Requirements {Requirement("DataSet", ValueType::DATA_SET, true)}); + auto root = make_shared(Requirements {{{"Root", ValueType::ENTITY, ds, true}}}); + + ErrorList errors; + entity::XmlParser parser; + + auto doc = R"DOC( + + + abc + 101 + 50.5 + + +)DOC"; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ("Root", entity->getName()); + auto set = entity->get("DataSet"); + ASSERT_EQ("abc", set.get("text")); + ASSERT_EQ(101, set.get("int")); + ASSERT_EQ(50.5, set.get("double")); +} + +TEST_F(EntityParserTest, should_parse_tables) +{ + auto table = make_shared(Requirements {Requirement("Table", ValueType::TABLE, true)}); + auto root = make_shared(Requirements {{{"Root", ValueType::ENTITY, table, true}}}); + + ErrorList errors; + entity::XmlParser parser; + + auto doc = R"DOC( + + + + abc + 101 + 50.5 + + + def + 102 + 100.5 + +
+
+)DOC"; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ("Root", entity->getName()); + auto set = entity->get("Table"); + auto e1 = set.get("A"); + + ASSERT_EQ("abc", e1.get("text")); + ASSERT_EQ(101, e1.get("int")); + ASSERT_EQ(50.5, e1.get("double")); + + auto e2 = set.get("B"); + + ASSERT_EQ("def", e2.get("text2")); + ASSERT_EQ(102, e2.get("int2")); + ASSERT_EQ(100.5, e2.get("double2")); +} diff --git a/test_package/kinematics_test.cpp b/test_package/kinematics_test.cpp index a2bf20d3..1c9c462c 100644 --- a/test_package/kinematics_test.cpp +++ b/test_package/kinematics_test.cpp @@ -36,6 +36,8 @@ using namespace std; using namespace mtconnect; using namespace mtconnect::entity; +inline DataSetEntry operator"" _E(const char *c, std::size_t) { return DataSetEntry(c); } + // main int main(int argc, char *argv[]) { @@ -251,3 +253,123 @@ TEST_F(KinematicsTest, RotaryJsonPrinting) ASSERT_EQ("The spindle kinematics", motion["Description"].get()); } } + +TEST_F(KinematicsTest, should_parse_kinematic_data_sets) +{ + m_agentTestHelper->createAgent("/samples/kinematics_2.5_data_set.xml", 8, 4, "2.5", 25); + m_agentId = to_string(getCurrentTimeInSec()); + m_device = m_agentTestHelper->m_agent->getDeviceByName("LinuxCNC"); + + ASSERT_NE(nullptr, m_device); + + auto rot = m_device->getComponentById("c"); + auto &ent = rot->get("Configuration"); + ASSERT_TRUE(ent); + auto motion = ent->get("Motion"); + + ASSERT_EQ("spin", motion->get("id")); + ASSERT_EQ("CONTINUOUS", motion->get("type")); + ASSERT_EQ("DIRECT", motion->get("actuation")); + ASSERT_EQ("machine", motion->get("coordinateSystemIdRef")); + ASSERT_EQ("zax", motion->get("parentIdRef")); + ASSERT_EQ("The spindle kinematics", motion->get("Description")); + + auto ds3 = motion->get("AxisDataSet"); + + ASSERT_EQ(0.0, get(ds3.find("X"_E)->m_value)); + ASSERT_EQ(0.5, get(ds3.find("Y"_E)->m_value)); + ASSERT_EQ(1.0, get(ds3.find("Z"_E)->m_value)); + + auto tf = motion->maybeGet("Transformation"); + ASSERT_TRUE(tf); + + auto ds1 = (*tf)->get("TranslationDataSet"); + + ASSERT_EQ(10.0, get(ds1.find("X"_E)->m_value)); + ASSERT_EQ(20.0, get(ds1.find("Y"_E)->m_value)); + ASSERT_EQ(30.0, get(ds1.find("Z"_E)->m_value)); + + auto ds2 = (*tf)->get("RotationDataSet"); + + ASSERT_EQ(90.0, get(ds2.find("A"_E)->m_value)); + ASSERT_EQ(0.0, get(ds2.find("B"_E)->m_value)); + ASSERT_EQ(180, get(ds2.find("C"_E)->m_value)); +} + +TEST_F(KinematicsTest, should_generate_data_xml_data_sets) +{ + m_agentTestHelper->createAgent("/samples/kinematics_2.5_data_set.xml", 8, 4, "2.5", 25); + + { + PARSE_XML_RESPONSE("/LinuxCNC/probe"); + + ASSERT_XML_PATH_COUNT(doc, ROTARY_MOTION_PATH, 1); + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "@id", "spin"); + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "@type", "CONTINUOUS"); + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "@parentIdRef", "zax"); + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "@actuation", "DIRECT"); + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "@coordinateSystemIdRef", "machine"); + + ASSERT_XML_PATH_EQUAL( + doc, ROTARY_MOTION_PATH "/m:Transformation/m:TranslationDataSet/m:Entry[@key='X']", "10"); + ASSERT_XML_PATH_EQUAL( + doc, ROTARY_MOTION_PATH "/m:Transformation/m:TranslationDataSet/m:Entry[@key='Y']", "20"); + ASSERT_XML_PATH_EQUAL( + doc, ROTARY_MOTION_PATH "/m:Transformation/m:TranslationDataSet/m:Entry[@key='Z']", "30"); + + ASSERT_XML_PATH_EQUAL( + doc, ROTARY_MOTION_PATH "/m:Transformation/m:RotationDataSet/m:Entry[@key='A']", "90"); + ASSERT_XML_PATH_EQUAL( + doc, ROTARY_MOTION_PATH "/m:Transformation/m:RotationDataSet/m:Entry[@key='B']", "0"); + ASSERT_XML_PATH_EQUAL( + doc, ROTARY_MOTION_PATH "/m:Transformation/m:RotationDataSet/m:Entry[@key='C']", "180"); + + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "/m:AxisDataSet/m:Entry[@key='X']", "0"); + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "/m:AxisDataSet/m:Entry[@key='Y']", "0.5"); + ASSERT_XML_PATH_EQUAL(doc, ROTARY_MOTION_PATH "/m:AxisDataSet/m:Entry[@key='Z']", "1"); + } +} + +TEST_F(KinematicsTest, should_generate_data_json_data_sets) +{ + m_agentTestHelper->createAgent("/samples/kinematics_2.5_data_set.xml", 8, 4, "2.5", 25); + + { + PARSE_JSON_RESPONSE("/LinuxCNC/probe"); + + auto devices = doc.at("/MTConnectDevices/Devices"_json_pointer); + auto device = devices.at(0).at("/Device"_json_pointer); + auto rotary = device.at("/Components/0/Axes/Components/1/Rotary"_json_pointer); + + auto motion = rotary.at("/Configuration/Motion"_json_pointer); + ASSERT_TRUE(motion.is_object()); + + ASSERT_EQ(8, motion.size()); + EXPECT_EQ("spin", motion["id"]); + EXPECT_EQ("CONTINUOUS", motion["type"]); + EXPECT_EQ("DIRECT", motion["actuation"]); + EXPECT_EQ("zax", motion["parentIdRef"]); + EXPECT_EQ("machine", motion["coordinateSystemIdRef"]); + + auto trans = motion.at("/Transformation/TranslationDataSet"_json_pointer); + ASSERT_TRUE(trans.is_object()); + ASSERT_EQ(3, trans.size()); + ASSERT_EQ(10.0, trans["X"].get()); + ASSERT_EQ(20.0, trans["Y"].get()); + ASSERT_EQ(30.0, trans["Z"].get()); + + auto rot = motion.at("/Transformation/RotationDataSet"_json_pointer); + ASSERT_TRUE(rot.is_object()); + ASSERT_EQ(3, rot.size()); + ASSERT_EQ(90.0, rot["A"].get()); + ASSERT_EQ(0.0, rot["B"].get()); + ASSERT_EQ(180.0, rot["C"].get()); + + auto axis = motion.at("/AxisDataSet"_json_pointer); + ASSERT_TRUE(axis.is_object()); + ASSERT_EQ(3, axis.size()); + ASSERT_EQ(0.0, axis["X"].get()); + ASSERT_EQ(0.5, axis["Y"].get()); + ASSERT_EQ(1.0, axis["Z"].get()); + } +} diff --git a/test_package/resources/samples/kinematics_2.5_data_set.xml b/test_package/resources/samples/kinematics_2.5_data_set.xml new file mode 100644 index 00000000..6ba99558 --- /dev/null +++ b/test_package/resources/samples/kinematics_2.5_data_set.xml @@ -0,0 +1,84 @@ + + +
+ + + + + + + + 101 + 102 + 103 + + + + + + 10 + 10 + 10 + + + 90 + 0 + 90 + + + + + + + + + + + + + + + The linears Z kinematics + + 100.0 + 101.0 + 102.0 + + + 0.0 + 0.1 + 1.0 + + + + + + + + The spindle kinematics + + + 10.0 + 20.0 + 30.0 + + + 90.0 + 0.0 + 180 + + + + 0.0 + 0.5 + 1.0 + + + + + + + + + +