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
+
+
+
+
+
+
+
+
+
+