|
10 | 10 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
11 | 11 | */
|
12 | 12 |
|
| 13 | +#include <cstring> |
13 | 14 | #include <functional>
|
| 15 | +#include <iostream> |
14 | 16 | #include <list>
|
| 17 | +#include <sstream> |
| 18 | +#include <string> |
| 19 | +#include <typeindex> |
15 | 20 |
|
16 | 21 | #if defined(__linux) || defined(__linux__)
|
17 | 22 | #pragma GCC diagnostic push
|
|
39 | 44 | #include "behaviortree_cpp/tree_node.h"
|
40 | 45 | #include "behaviortree_cpp/utils/demangle_util.h"
|
41 | 46 |
|
| 47 | +namespace |
| 48 | +{ |
| 49 | +std::string |
| 50 | +xsdAttributeType(const BT::PortInfo& port_info) |
| 51 | +{ |
| 52 | + const auto& type_info = port_info.type(); |
| 53 | + if ((type_info == typeid(int)) or (type_info == typeid(unsigned int))) |
| 54 | + { |
| 55 | + return "integerOrBlackboardType"; |
| 56 | + } |
| 57 | + else if (type_info == typeid(double)) |
| 58 | + { |
| 59 | + return "decimalOrBlackboardType"; |
| 60 | + } |
| 61 | + else if (type_info == typeid(bool)) |
| 62 | + { |
| 63 | + return "xs:boolean"; |
| 64 | + } |
| 65 | + else if (type_info == typeid(std::string)) |
| 66 | + { |
| 67 | + return "xs:string"; |
| 68 | + } |
| 69 | + |
| 70 | + return std::string(); |
| 71 | +} |
| 72 | + |
| 73 | +} // Anonymous workspace |
| 74 | + |
42 | 75 | namespace BT
|
43 | 76 | {
|
44 | 77 | using namespace tinyxml2;
|
@@ -1117,6 +1150,269 @@ std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory,
|
1117 | 1150 | return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
|
1118 | 1151 | }
|
1119 | 1152 |
|
| 1153 | +std::string writeTreeXSD(const BehaviorTreeFactory& factory) |
| 1154 | +{ |
| 1155 | + // There are 2 forms of representation for a node: |
| 1156 | + // compact: <Sequence .../> and explicit: <Control ID="Sequence" ... /> |
| 1157 | + // Only the compact form is supported because the explicit form doesn't |
| 1158 | + // make sense with XSD since we would need to allow any attribute. |
| 1159 | + // Prepare the data |
| 1160 | + |
| 1161 | + std::map<std::string, const TreeNodeManifest*> ordered_models; |
| 1162 | + for (const auto& [registration_id, model] : factory.manifests()) |
| 1163 | + { |
| 1164 | + ordered_models.insert({registration_id, &model}); |
| 1165 | + } |
| 1166 | + |
| 1167 | + XMLDocument doc; |
| 1168 | + |
| 1169 | + // Add the XML declaration |
| 1170 | + XMLDeclaration* declaration = doc.NewDeclaration("xml version=\"1.0\" encoding=\"UTF-8\""); |
| 1171 | + doc.InsertFirstChild(declaration); |
| 1172 | + |
| 1173 | + // Create the root element with namespace and attributes |
| 1174 | + // To validate a BT XML file with `schema.xsd` in the same directory: |
| 1175 | + // <root BTCPP_format="4" main_tree_to_execute="MainTree" |
| 1176 | + // xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| 1177 | + // xsi:noNamespaceSchemaLocation="schema.xsd"> |
| 1178 | + XMLElement* schema_element = doc.NewElement("xs:schema"); |
| 1179 | + schema_element->SetAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema"); |
| 1180 | + schema_element->SetAttribute("elementFormDefault", "qualified"); |
| 1181 | + doc.InsertEndChild(schema_element); |
| 1182 | + |
| 1183 | + auto parse_and_insert = [&doc](XMLElement* parent_elem, const char* str) { |
| 1184 | + XMLDocument tmp_doc; |
| 1185 | + tmp_doc.Parse(str); |
| 1186 | + if (tmp_doc.Error()) |
| 1187 | + { |
| 1188 | + std::cerr << "Internal error parsing existing XML: " << tmp_doc.ErrorStr() << std::endl; |
| 1189 | + return; |
| 1190 | + } |
| 1191 | + for (auto child = tmp_doc.FirstChildElement(); child != nullptr; |
| 1192 | + child = child->NextSiblingElement()) |
| 1193 | + { |
| 1194 | + parent_elem->InsertEndChild(child->DeepClone(&doc)); |
| 1195 | + } |
| 1196 | + }; |
| 1197 | + |
| 1198 | + // Common elements. |
| 1199 | + XMLComment* comment = doc.NewComment("Define the common elements"); |
| 1200 | + schema_element->InsertEndChild(comment); |
| 1201 | + |
| 1202 | + // TODO: add <xs:whiteSpace value="preserve"/> for `inputPortType` and `outputPortType`. |
| 1203 | + parse_and_insert(schema_element, R"( |
| 1204 | + <xs:simpleType name="decimalOrBlackboardType"> |
| 1205 | + <xs:restriction base="xs:string"> |
| 1206 | + <xs:pattern value="[+-]?[0-9]*(\.[0-9]+)?"/> |
| 1207 | + <xs:pattern value="\{[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*\}"/> |
| 1208 | + </xs:restriction> |
| 1209 | + </xs:simpleType> |
| 1210 | + <xs:simpleType name="integerOrBlackboardType"> |
| 1211 | + <xs:restriction base="xs:string"> |
| 1212 | + <xs:pattern value="[+-]?[0-9]+"/> |
| 1213 | + <xs:pattern value="\{[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*\}"/> |
| 1214 | + </xs:restriction> |
| 1215 | + </xs:simpleType> |
| 1216 | + <xs:simpleType name="descriptionType"> |
| 1217 | + <xs:restriction base="xs:string"> |
| 1218 | + <xs:whiteSpace value="preserve"/> |
| 1219 | + </xs:restriction> |
| 1220 | + </xs:simpleType> |
| 1221 | + <xs:complexType name="inputPortType"> |
| 1222 | + <xs:simpleContent> |
| 1223 | + <xs:extension base="xs:string"> |
| 1224 | + <xs:attribute name="name" type="xs:string" use="required"/> |
| 1225 | + <xs:attribute name="type" type="xs:string" use="optional"/> |
| 1226 | + <xs:attribute name="default" type="xs:string" use="optional"/> |
| 1227 | + </xs:extension> |
| 1228 | + </xs:simpleContent> |
| 1229 | + </xs:complexType> |
| 1230 | + <xs:complexType name="outputPortType"> |
| 1231 | + <xs:simpleContent> |
| 1232 | + <xs:extension base="xs:string"> |
| 1233 | + <xs:attribute name="name" type="xs:string" use="required"/> |
| 1234 | + <xs:attribute name="type" type="xs:string" use="optional"/> |
| 1235 | + </xs:extension> |
| 1236 | + </xs:simpleContent> |
| 1237 | + </xs:complexType> |
| 1238 | + <xs:attributeGroup name="preconditionAttributeGroup"> |
| 1239 | + <xs:attribute name="_failureIf" type="xs:string" use="optional"/> |
| 1240 | + <xs:attribute name="_skipIf" type="xs:string" use="optional"/> |
| 1241 | + <xs:attribute name="_successIf" type="xs:string" use="optional"/> |
| 1242 | + <xs:attribute name="_while" type="xs:string" use="optional"/> |
| 1243 | + </xs:attributeGroup> |
| 1244 | + <xs:attributeGroup name="postconditionAttributeGroup"> |
| 1245 | + <xs:attribute name="_onSuccess" type="xs:string" use="optional"/> |
| 1246 | + <xs:attribute name="_onFailure" type="xs:string" use="optional"/> |
| 1247 | + <xs:attribute name="_post" type="xs:string" use="optional"/> |
| 1248 | + <xs:attribute name="_onHalted" type="xs:string" use="optional"/> |
| 1249 | + </xs:attributeGroup>)"); |
| 1250 | + |
| 1251 | + // Common attributes |
| 1252 | + // Note that we do not add the `ID` attribute because we do not |
| 1253 | + // support the explicit notation (e.g. <Action ID="Saysomething">). |
| 1254 | + // Cf. https://www.behaviortree.dev/docs/learn-the-basics/xml_format/#compact-vs-explicit-representation |
| 1255 | + // There is no way to check attribute validity with the explicit notation with XSD. |
| 1256 | + // The `ID` attribute for `<SubTree>` is handled separately. |
| 1257 | + parse_and_insert(schema_element, R"( |
| 1258 | + <xs:attributeGroup name="commonAttributeGroup"> |
| 1259 | + <xs:attribute name="name" type="xs:string" use="optional"/> |
| 1260 | + <xs:attributeGroup ref="preconditionAttributeGroup"/> |
| 1261 | + <xs:attributeGroup ref="postconditionAttributeGroup"/> |
| 1262 | + </xs:attributeGroup>)"); |
| 1263 | + |
| 1264 | + // Basic node types |
| 1265 | + parse_and_insert(schema_element, R"( |
| 1266 | + <xs:complexType name="treeNodesModelNodeType"> |
| 1267 | + <xs:sequence> |
| 1268 | + <xs:choice minOccurs="0" maxOccurs="unbounded"> |
| 1269 | + <xs:element name="input_port" type="inputPortType"/> |
| 1270 | + <xs:element name="output_port" type="outputPortType"/> |
| 1271 | + </xs:choice> |
| 1272 | + <xs:element name="description" type="descriptionType" minOccurs="0" maxOccurs="1"/> |
| 1273 | + </xs:sequence> |
| 1274 | + <xs:attribute name="ID" type="xs:string" use="required"/> |
| 1275 | + </xs:complexType> |
| 1276 | + <xs:group name="treeNodesModelNodeGroup"> |
| 1277 | + <xs:choice> |
| 1278 | + <xs:element name="Action" type="treeNodesModelNodeType"/> |
| 1279 | + <xs:element name="Condition" type="treeNodesModelNodeType"/> |
| 1280 | + <xs:element name="Control" type="treeNodesModelNodeType"/> |
| 1281 | + <xs:element name="Decorator" type="treeNodesModelNodeType"/> |
| 1282 | + </xs:choice> |
| 1283 | + </xs:group> |
| 1284 | + )"); |
| 1285 | + |
| 1286 | + // `root` element |
| 1287 | + const auto root_element_xsd = R"( |
| 1288 | + <xs:element name="root"> |
| 1289 | + <xs:complexType> |
| 1290 | + <xs:sequence> |
| 1291 | + <xs:choice minOccurs="0" maxOccurs="unbounded"> |
| 1292 | + <xs:element ref="include"/> |
| 1293 | + <xs:element ref="BehaviorTree"/> |
| 1294 | + </xs:choice> |
| 1295 | + <xs:element ref="TreeNodesModel" minOccurs="0" maxOccurs="1"/> |
| 1296 | + </xs:sequence> |
| 1297 | + <xs:attribute name="BTCPP_format" type="xs:string" use="required"/> |
| 1298 | + <xs:attribute name="main_tree_to_execute" type="xs:string" use="required"/> |
| 1299 | + </xs:complexType> |
| 1300 | + </xs:element> |
| 1301 | + )"; |
| 1302 | + parse_and_insert(schema_element, root_element_xsd); |
| 1303 | + |
| 1304 | + // Group definition for a single node of any of the existing node types. |
| 1305 | + XMLElement* one_node_group = doc.NewElement("xs:group"); |
| 1306 | + { |
| 1307 | + one_node_group->SetAttribute("name", "oneNodeGroup"); |
| 1308 | + std::ostringstream xsd; |
| 1309 | + xsd << "<xs:choice>"; |
| 1310 | + for (const auto& [registration_id, model] : ordered_models) |
| 1311 | + { |
| 1312 | + xsd << "<xs:element name=\"" << registration_id << "\" type=\"" << registration_id << "Type\"/>"; |
| 1313 | + } |
| 1314 | + xsd << "</xs:choice>"; |
| 1315 | + parse_and_insert(one_node_group, xsd.str().c_str()); |
| 1316 | + schema_element->InsertEndChild(one_node_group); |
| 1317 | + } |
| 1318 | + |
| 1319 | + // `include` element |
| 1320 | + parse_and_insert(schema_element, R"( |
| 1321 | + <xs:element name="include"> |
| 1322 | + <xs:complexType> |
| 1323 | + <xs:attribute name="path" type="xs:string" use="required"/> |
| 1324 | + <xs:attribute name="ros_pkg" type="xs:string" use="optional"/> |
| 1325 | + </xs:complexType> |
| 1326 | + </xs:element> |
| 1327 | + )"); |
| 1328 | + |
| 1329 | + // `BehaviorTree` element |
| 1330 | + parse_and_insert(schema_element, R"( |
| 1331 | + <xs:element name="BehaviorTree"> |
| 1332 | + <xs:complexType> |
| 1333 | + <xs:group ref="oneNodeGroup"/> |
| 1334 | + <xs:attribute name="ID" type="xs:string" use="required"/> |
| 1335 | + </xs:complexType> |
| 1336 | + </xs:element> |
| 1337 | + )"); |
| 1338 | + |
| 1339 | + // `TreeNodesModel` element |
| 1340 | + parse_and_insert(schema_element, R"( |
| 1341 | + <xs:element name="TreeNodesModel"> |
| 1342 | + <xs:complexType> |
| 1343 | + <xs:group ref="treeNodesModelNodeGroup" minOccurs="0" maxOccurs="unbounded"/> |
| 1344 | + </xs:complexType> |
| 1345 | + </xs:element> |
| 1346 | + )"); |
| 1347 | + |
| 1348 | + // Definitions for all node types. |
| 1349 | + for (const auto& [registration_id, model] : ordered_models) |
| 1350 | + { |
| 1351 | + XMLElement* type = doc.NewElement("xs:complexType"); |
| 1352 | + type->SetAttribute("name", (model->registration_ID + "Type").c_str()); |
| 1353 | + if ((model->type == NodeType::CONDITION) or (model->type == NodeType::ACTION)) |
| 1354 | + { |
| 1355 | + /* No children, nothing to add. */ |
| 1356 | + } |
| 1357 | + else if ((model->type == NodeType::DECORATOR) |
| 1358 | + or (model->type == NodeType::SUBTREE)) |
| 1359 | + { |
| 1360 | + /* One child. */ |
| 1361 | + // <xs:group ref="oneNodeGroup" minOccurs="0" maxOccurs="1"/> |
| 1362 | + XMLElement* group = doc.NewElement("xs:group"); |
| 1363 | + group->SetAttribute("ref", "oneNodeGroup"); |
| 1364 | + group->SetAttribute("minOccurs", "0"); |
| 1365 | + group->SetAttribute("maxOccurs", "1"); |
| 1366 | + type->InsertEndChild(group); |
| 1367 | + } |
| 1368 | + else |
| 1369 | + { |
| 1370 | + /* NodeType::CONTROL. */ |
| 1371 | + // TODO: check the code, the doc says 1..N but why not 0..N? |
| 1372 | + // <xs:group ref="oneNodeGroup" minOccurs="0" maxOccurs="unbounded"/> |
| 1373 | + XMLElement* group = doc.NewElement("xs:group"); |
| 1374 | + group->SetAttribute("ref", "oneNodeGroup"); |
| 1375 | + group->SetAttribute("minOccurs", "0"); |
| 1376 | + group->SetAttribute("maxOccurs", "unbounded"); |
| 1377 | + type->InsertEndChild(group); |
| 1378 | + } |
| 1379 | + XMLElement* common_attr_group = doc.NewElement("xs:attributeGroup"); |
| 1380 | + common_attr_group->SetAttribute("ref", "commonAttributeGroup"); |
| 1381 | + type->InsertEndChild(common_attr_group); |
| 1382 | + for (const auto& [port_name, port_info] : model->ports) |
| 1383 | + { |
| 1384 | + XMLElement* attr = doc.NewElement("xs:attribute"); |
| 1385 | + attr->SetAttribute("name", port_name.c_str()); |
| 1386 | + const auto xsd_attribute_type = xsdAttributeType(port_info); |
| 1387 | + if (not xsd_attribute_type.empty()) |
| 1388 | + { |
| 1389 | + attr->SetAttribute("type", xsd_attribute_type.c_str()); |
| 1390 | + } |
| 1391 | + if (not port_info.defaultValue().empty()) |
| 1392 | + { |
| 1393 | + attr->SetAttribute("default", port_info.defaultValueString().c_str()); |
| 1394 | + } |
| 1395 | + else |
| 1396 | + { |
| 1397 | + attr->SetAttribute("use", "required"); |
| 1398 | + } |
| 1399 | + type->InsertEndChild(attr); |
| 1400 | + } |
| 1401 | + if (model->registration_ID == "SubTree") |
| 1402 | + { |
| 1403 | + parse_and_insert(type, R"( |
| 1404 | + <xs:attribute name="ID" type="xs:string" use="required"/> |
| 1405 | + <xs:anyAttribute processContents="skip"/> |
| 1406 | + )"); |
| 1407 | + } |
| 1408 | + schema_element->InsertEndChild(type); |
| 1409 | + } |
| 1410 | + |
| 1411 | + XMLPrinter printer; |
| 1412 | + doc.Print(&printer); |
| 1413 | + return std::string(printer.CStr(), size_t(printer.CStrSize() - 1)); |
| 1414 | +} |
| 1415 | + |
1120 | 1416 | Tree buildTreeFromText(const BehaviorTreeFactory& factory, const std::string& text,
|
1121 | 1417 | const Blackboard::Ptr& blackboard)
|
1122 | 1418 | {
|
|
0 commit comments