From 9fdcda194adff47eba19dd267f084712d45a0e7c Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 12 Aug 2023 23:09:20 -0600 Subject: [PATCH 01/43] Implement minimal Python bindings to SyncActionNode --- CMakeLists.txt | 8 ++ python_examples/btpy.py | 18 ++++ python_examples/ex01_sample.py | 52 ++++++++++ python_examples/ex02_generic_data.py | 77 ++++++++++++++ src/python_bindings.cpp | 147 +++++++++++++++++++++++++++ 5 files changed, 302 insertions(+) create mode 100644 python_examples/btpy.py create mode 100644 python_examples/ex01_sample.py create mode 100644 python_examples/ex02_generic_data.py create mode 100644 src/python_bindings.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 77592e776..313ad50f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ option(BTCPP_EXAMPLES "Build tutorials and examples" ON) option(BTCPP_UNIT_TESTS "Build the unit tests" ON) option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON) option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON) +option(BTCPP_PYTHON "Add Python bindings" ON) option(USE_V3_COMPATIBLE_NAMES "Use some alias to compile more easily old 3.x code" OFF) @@ -134,6 +135,13 @@ if(BTCPP_SQLITE_LOGGING) list(APPEND BT_SOURCE src/loggers/bt_sqlite_logger.cpp ) endif() +if(BTCPP_PYTHON) + find_package(Python COMPONENTS Interpreter Development) + find_package(pybind11 CONFIG) + pybind11_add_module(btpy_cpp src/python_bindings.cpp) + target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY}) +endif() + ###################################################### if (UNIX) diff --git a/python_examples/btpy.py b/python_examples/btpy.py new file mode 100644 index 000000000..b296a1265 --- /dev/null +++ b/python_examples/btpy.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +""" +Top-level module of the BehaviorTree.CPP Python bindings. +""" + +from btpy_cpp import * # re-export + + +def ports(inputs=[], outputs=[]): + """Decorator to specify input and outputs ports for an action node.""" + + def specify_ports(cls): + cls.input_ports = list(inputs) + cls.output_ports = list(outputs) + return cls + + return specify_ports diff --git a/python_examples/ex01_sample.py b/python_examples/ex01_sample.py new file mode 100644 index 000000000..3a89c4136 --- /dev/null +++ b/python_examples/ex01_sample.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +""" +Demo adapted from [1]. + +To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` +variable. It is probably located in your build directory if you're building from +source. + +[1]: https://github.com/BehaviorTree/btcpp_sample +""" + +from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports + + +xml_text = """ + + + + + + + + + + + + +""" + + +@ports(inputs=["message"]) +class SaySomething(SyncActionNode): + def tick(self): + msg = self.get_input("message") + print(msg) + return NodeStatus.SUCCESS + + +@ports(outputs=["text"]) +class ThinkWhatToSay(SyncActionNode): + def tick(self): + self.set_output("text", "The answer is 42") + return NodeStatus.SUCCESS + + +factory = BehaviorTreeFactory() +factory.register(SaySomething) +factory.register(ThinkWhatToSay) + +tree = factory.create_tree_from_text(xml_text) +tree.tick_while_running() diff --git a/python_examples/ex02_generic_data.py b/python_examples/ex02_generic_data.py new file mode 100644 index 000000000..5af53acfd --- /dev/null +++ b/python_examples/ex02_generic_data.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +""" +Demonstration of passing generic data between nodes. + +To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` +variable. It is probably located in your build directory if you're building from +source. +""" + +import numpy as np +from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports + + +xml_text = """ + + + + + + + + + + + + +""" + + +@ports(inputs=["position", "theta"], outputs=["out"]) +class Rotate(SyncActionNode): + def tick(self): + # Build a rotation matrix which rotates points by `theta` degrees. + theta = np.deg2rad(self.get_input("theta")) + c, s = np.cos(theta), np.sin(theta) + M = np.array([[c, -s], [s, c]]) + + # Apply the rotation to the input position. + position = self.get_input("position") + rotated = M @ position + + # Set the output. + self.set_output("out", rotated) + + return NodeStatus.SUCCESS + + +@ports(inputs=["position", "offset"], outputs=["out"]) +class Translate(SyncActionNode): + def tick(self): + offset = np.asarray(self.get_input("offset")) + + # Apply the translation to the input position. + position = np.asarray(self.get_input("position")) + translated = position + offset + + # Set the output. + self.set_output("out", translated) + + return NodeStatus.SUCCESS + + +@ports(inputs=["value"]) +class Print(SyncActionNode): + def tick(self): + print(self.get_input("value")) + return NodeStatus.SUCCESS + + +factory = BehaviorTreeFactory() +factory.register(Rotate) +factory.register(Translate) +factory.register(Print) + +tree = factory.create_tree_from_text(xml_text) +tree.tick_while_running() diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp new file mode 100644 index 000000000..ef5e35a83 --- /dev/null +++ b/src/python_bindings.cpp @@ -0,0 +1,147 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "behaviortree_cpp/basic_types.h" +#include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp/action_node.h" +#include "behaviortree_cpp/tree_node.h" + +namespace BT +{ + +namespace py = pybind11; + +class Py_SyncActionNode : public SyncActionNode +{ +public: + Py_SyncActionNode(const std::string& name, const NodeConfig& config) : + SyncActionNode(name, config) + {} + + NodeStatus tick() override + { + py::gil_scoped_acquire gil; + return py::get_overload(this, "tick")().cast(); + } + + py::object Py_getInput(const std::string& name) + { + py::object obj; + getInput(name, obj); + return obj; + } + + void Py_setOutput(const std::string& name, const py::object& value) + { + setOutput(name, value); + } +}; + +// Add a conversion specialization from string values into general py::objects +// by evaluating as a Python expression. +template <> +inline py::object convertFromString(StringView str) +{ + try + { + // First, try evaluating the string as-is. Maybe it's a number, a list, a + // dict, an object, etc. + return py::eval(str); + } + catch (py::error_already_set& e) + { + // If that fails, then assume it's a string literal with quotation marks + // omitted. + return py::str(str); + } +} + +PYBIND11_MODULE(btpy_cpp, m) +{ + py::class_(m, "PortInfo"); + m.def("input_port", + [](const std::string& name) { return InputPort(name); }); + m.def("output_port", + [](const std::string& name) { return OutputPort(name); }); + + m.def( + "ports2", + [](const py::list& inputs, const py::list& outputs) -> auto { + return [](py::type type) -> auto { return type; }; + }, + py::kw_only(), py::arg("inputs") = py::none(), py::arg("outputs") = py::none()); + + py::class_(m, "BehaviorTreeFactory") + .def(py::init()) + .def("register", + [](BehaviorTreeFactory& factory, const py::type type) { + const std::string name = type.attr("__name__").cast(); + + TreeNodeManifest manifest; + manifest.type = NodeType::ACTION; + manifest.registration_ID = name; + manifest.ports = {}; + manifest.description = ""; + + const auto input_ports = type.attr("input_ports").cast(); + for (const auto& name : input_ports) + { + manifest.ports.insert(InputPort(name.cast())); + } + + const auto output_ports = type.attr("output_ports").cast(); + for (const auto& name : output_ports) + { + manifest.ports.insert(OutputPort(name.cast())); + } + + factory.registerBuilder( + manifest, + [type](const std::string& name, + const NodeConfig& config) -> std::unique_ptr { + py::object obj = type(name, config); + // TODO: Increment the object's reference count or else it + // will be GC'd at the end of this scope. The downside is + // that, unless we can decrement the ref when the unique_ptr + // is destroyed, then the object will live forever. + obj.inc_ref(); + + return std::unique_ptr( + obj.cast()); + }); + }) + .def("create_tree_from_text", + [](BehaviorTreeFactory& factory, const std::string& text) -> Tree { + return factory.createTreeFromText(text); + }); + + py::class_(m, "Tree") + .def("tick_once", &Tree::tickOnce) + .def("tick_exactly_once", &Tree::tickExactlyOnce) + .def("tick_while_running", &Tree::tickWhileRunning, + py::arg("sleep_time") = std::chrono::milliseconds(10)); + + py::enum_(m, "NodeStatus") + .value("SUCCESS", NodeStatus::SUCCESS) + .value("FAILURE", NodeStatus::FAILURE) + .value("IDLE", NodeStatus::IDLE) + .value("RUNNING", NodeStatus::RUNNING) + .value("SKIPPED", NodeStatus::SKIPPED) + .export_values(); + + py::class_(m, "NodeConfig"); + + py::class_(m, "SyncActionNode") + .def(py::init()) + .def("tick", &Py_SyncActionNode::tick) + .def("get_input", &Py_SyncActionNode::Py_getInput) + .def("set_output", &Py_SyncActionNode::Py_setOutput); +} + +} // namespace BT From f560500011c1c478c665643907324123fffcf82f Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 00:22:11 -0600 Subject: [PATCH 02/43] Add stateful action bindings. --- python_examples/ex03_stateful_nodes.py | 75 ++++++++++++++++++++++++++ src/python_bindings.cpp | 63 +++++++++++++++++++++- 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 python_examples/ex03_stateful_nodes.py diff --git a/python_examples/ex03_stateful_nodes.py b/python_examples/ex03_stateful_nodes.py new file mode 100644 index 000000000..d646a8649 --- /dev/null +++ b/python_examples/ex03_stateful_nodes.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +""" +Demonstration of stateful action nodes. + +To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` +variable. It is probably located in your build directory if you're building from +source. +""" + +import numpy as np +from btpy import ( + BehaviorTreeFactory, + StatefulActionNode, + SyncActionNode, + NodeStatus, + ports, +) + + +xml_text = """ + + + + + + + + + + + + + + + + +""" + + +@ports(inputs=["x0", "x1"], outputs=["out"]) +class Interpolate(StatefulActionNode): + def on_start(self): + self.t = 0.0 + self.x0 = np.asarray(self.get_input("x0")) + self.x1 = np.asarray(self.get_input("x1")) + return NodeStatus.RUNNING + + def on_running(self): + if self.t < 1.0: + x = (1.0 - self.t) * self.x0 + self.t * self.x1 + self.set_output("out", x) + self.t += 0.1 + return NodeStatus.RUNNING + else: + return NodeStatus.SUCCESS + + def on_halted(self): + pass + + +@ports(inputs=["value"]) +class Print(SyncActionNode): + def tick(self): + print(self.get_input("value")) + return NodeStatus.SUCCESS + + +factory = BehaviorTreeFactory() +factory.register(Interpolate) +factory.register(Print) + +tree = factory.create_tree_from_text(xml_text) +tree.tick_while_running() diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index ef5e35a83..98af4070e 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -43,6 +44,46 @@ class Py_SyncActionNode : public SyncActionNode } }; +class Py_StatefulActionNode final : public StatefulActionNode +{ +public: + Py_StatefulActionNode(const std::string& name, const NodeConfig& config) : + StatefulActionNode(name, config) + {} + + NodeStatus onStart() override + { + py::gil_scoped_acquire gil; + return py::get_overload(this, "on_start")().cast(); + } + + NodeStatus onRunning() override + { + py::gil_scoped_acquire gil; + return py::get_overload(this, "on_running")().cast(); + } + + void onHalted() override + { + py::gil_scoped_acquire gil; + py::get_overload(this, "on_halted")(); + } + + // TODO: Share these duplicated methods with other node types. + py::object Py_getInput(const std::string& name) + { + py::object obj; + getInput(name, obj); + return obj; + } + + // TODO: Share these duplicated methods with other node types. + void Py_setOutput(const std::string& name, const py::object& value) + { + setOutput(name, value); + } +}; + // Add a conversion specialization from string values into general py::objects // by evaluating as a Python expression. template <> @@ -112,8 +153,18 @@ PYBIND11_MODULE(btpy_cpp, m) // is destroyed, then the object will live forever. obj.inc_ref(); - return std::unique_ptr( - obj.cast()); + if (py::isinstance(obj)) + { + return std::unique_ptr(obj.cast()); + } + else if (py::isinstance(obj)) + { + return std::unique_ptr(obj.cast()); + } + else + { + throw std::runtime_error("invalid node type of " + name); + } }); }) .def("create_tree_from_text", @@ -142,6 +193,14 @@ PYBIND11_MODULE(btpy_cpp, m) .def("tick", &Py_SyncActionNode::tick) .def("get_input", &Py_SyncActionNode::Py_getInput) .def("set_output", &Py_SyncActionNode::Py_setOutput); + + py::class_(m, "StatefulActionNode") + .def(py::init()) + .def("on_start", &Py_StatefulActionNode::onStart) + .def("on_running", &Py_StatefulActionNode::onRunning) + .def("on_halted", &Py_StatefulActionNode::onHalted) + .def("get_input", &Py_StatefulActionNode::Py_getInput) + .def("set_output", &Py_StatefulActionNode::Py_setOutput); } } // namespace BT From 1f9db33038c0c6f1ed75ef8a09f0d1ffa6c76a2a Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 10:30:18 -0600 Subject: [PATCH 03/43] Eliminate some code duplication. --- src/python_bindings.cpp | 50 ++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index 98af4070e..a47996918 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -30,18 +30,6 @@ class Py_SyncActionNode : public SyncActionNode py::gil_scoped_acquire gil; return py::get_overload(this, "tick")().cast(); } - - py::object Py_getInput(const std::string& name) - { - py::object obj; - getInput(name, obj); - return obj; - } - - void Py_setOutput(const std::string& name, const py::object& value) - { - setOutput(name, value); - } }; class Py_StatefulActionNode final : public StatefulActionNode @@ -68,21 +56,21 @@ class Py_StatefulActionNode final : public StatefulActionNode py::gil_scoped_acquire gil; py::get_overload(this, "on_halted")(); } +}; - // TODO: Share these duplicated methods with other node types. - py::object Py_getInput(const std::string& name) - { - py::object obj; - getInput(name, obj); - return obj; - } +template +py::object Py_getInput(const T& node, const std::string& name) +{ + py::object obj; + node.getInput(name, obj); + return obj; +} - // TODO: Share these duplicated methods with other node types. - void Py_setOutput(const std::string& name, const py::object& value) - { - setOutput(name, value); - } -}; +template +void Py_setOutput(T& node, const std::string& name, const py::object& value) +{ + node.setOutput(name, value); +} // Add a conversion specialization from string values into general py::objects // by evaluating as a Python expression. @@ -190,17 +178,17 @@ PYBIND11_MODULE(btpy_cpp, m) py::class_(m, "SyncActionNode") .def(py::init()) - .def("tick", &Py_SyncActionNode::tick) - .def("get_input", &Py_SyncActionNode::Py_getInput) - .def("set_output", &Py_SyncActionNode::Py_setOutput); + .def("get_input", &Py_getInput) + .def("set_output", &Py_setOutput) + .def("tick", &Py_SyncActionNode::tick); py::class_(m, "StatefulActionNode") .def(py::init()) + .def("get_input", &Py_getInput) + .def("set_output", &Py_setOutput) .def("on_start", &Py_StatefulActionNode::onStart) .def("on_running", &Py_StatefulActionNode::onRunning) - .def("on_halted", &Py_StatefulActionNode::onHalted) - .def("get_input", &Py_StatefulActionNode::Py_getInput) - .def("set_output", &Py_StatefulActionNode::Py_setOutput); + .def("on_halted", &Py_StatefulActionNode::onHalted); } } // namespace BT From 351c33a934df3b60a47ac972b941772891acc410 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 10:54:25 -0600 Subject: [PATCH 04/43] Use proper PYBIND11_OVERRIDE macros. --- src/python_bindings.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index a47996918..61414a551 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -27,8 +27,7 @@ class Py_SyncActionNode : public SyncActionNode NodeStatus tick() override { - py::gil_scoped_acquire gil; - return py::get_overload(this, "tick")().cast(); + PYBIND11_OVERRIDE_PURE_NAME(NodeStatus, Py_SyncActionNode, "tick", tick); } }; @@ -41,20 +40,18 @@ class Py_StatefulActionNode final : public StatefulActionNode NodeStatus onStart() override { - py::gil_scoped_acquire gil; - return py::get_overload(this, "on_start")().cast(); + PYBIND11_OVERRIDE_PURE_NAME(NodeStatus, Py_StatefulActionNode, "on_start", onStart); } NodeStatus onRunning() override { - py::gil_scoped_acquire gil; - return py::get_overload(this, "on_running")().cast(); + PYBIND11_OVERRIDE_PURE_NAME(NodeStatus, Py_StatefulActionNode, "on_running", + onRunning); } void onHalted() override { - py::gil_scoped_acquire gil; - py::get_overload(this, "on_halted")(); + PYBIND11_OVERRIDE_PURE_NAME(void, Py_StatefulActionNode, "on_running", onRunning); } }; From 4ff5673a5b7bcf485c572fc4864decbdd6c0425b Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 10:54:37 -0600 Subject: [PATCH 05/43] Export minimal set of identifiers to Python lib. --- python_examples/btpy.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python_examples/btpy.py b/python_examples/btpy.py index b296a1265..469e1bc7c 100644 --- a/python_examples/btpy.py +++ b/python_examples/btpy.py @@ -4,7 +4,14 @@ Top-level module of the BehaviorTree.CPP Python bindings. """ -from btpy_cpp import * # re-export +# re-export +from btpy_cpp import ( + BehaviorTreeFactory, + NodeStatus, + StatefulActionNode, + SyncActionNode, + Tree, +) def ports(inputs=[], outputs=[]): From 17da541bfafc82ccc9703fd9d4fedcf6087691cb Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 10:54:59 -0600 Subject: [PATCH 06/43] Clean up port handling. --- src/python_bindings.cpp | 118 ++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index 61414a551..a956ca8c9 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -55,16 +55,14 @@ class Py_StatefulActionNode final : public StatefulActionNode } }; -template -py::object Py_getInput(const T& node, const std::string& name) +py::object Py_getInput(const TreeNode& node, const std::string& name) { py::object obj; node.getInput(name, obj); return obj; } -template -void Py_setOutput(T& node, const std::string& name, const py::object& value) +void Py_setOutput(TreeNode& node, const std::string& name, const py::object& value) { node.setOutput(name, value); } @@ -88,21 +86,49 @@ inline py::object convertFromString(StringView str) } } -PYBIND11_MODULE(btpy_cpp, m) +PortsList extractPortsList(const py::type& type) { - py::class_(m, "PortInfo"); - m.def("input_port", - [](const std::string& name) { return InputPort(name); }); - m.def("output_port", - [](const std::string& name) { return OutputPort(name); }); - - m.def( - "ports2", - [](const py::list& inputs, const py::list& outputs) -> auto { - return [](py::type type) -> auto { return type; }; - }, - py::kw_only(), py::arg("inputs") = py::none(), py::arg("outputs") = py::none()); + PortsList ports; + + const auto input_ports = type.attr("input_ports").cast(); + for (const auto& name : input_ports) + { + ports.insert(InputPort(name.cast())); + } + const auto output_ports = type.attr("output_ports").cast(); + for (const auto& name : output_ports) + { + ports.insert(OutputPort(name.cast())); + } + + return ports; +} + +NodeBuilder makeTreeNodeBuilderFn(const py::type& type) +{ + return [type](const auto& name, const auto& config) -> auto { + py::object obj = type(name, config); + + // TODO: Increment the object's reference count or else it + // will be GC'd at the end of this scope. The downside is + // that, unless we can decrement the ref when the unique_ptr + // is destroyed, then the object will live forever. + obj.inc_ref(); + + if (py::isinstance(obj)) + { + return std::unique_ptr(obj.cast()); + } + else + { + throw std::runtime_error("invalid node type of " + name); + } + }; +} + +PYBIND11_MODULE(btpy_cpp, m) +{ py::class_(m, "BehaviorTreeFactory") .def(py::init()) .def("register", @@ -112,45 +138,10 @@ PYBIND11_MODULE(btpy_cpp, m) TreeNodeManifest manifest; manifest.type = NodeType::ACTION; manifest.registration_ID = name; - manifest.ports = {}; + manifest.ports = extractPortsList(type); manifest.description = ""; - const auto input_ports = type.attr("input_ports").cast(); - for (const auto& name : input_ports) - { - manifest.ports.insert(InputPort(name.cast())); - } - - const auto output_ports = type.attr("output_ports").cast(); - for (const auto& name : output_ports) - { - manifest.ports.insert(OutputPort(name.cast())); - } - - factory.registerBuilder( - manifest, - [type](const std::string& name, - const NodeConfig& config) -> std::unique_ptr { - py::object obj = type(name, config); - // TODO: Increment the object's reference count or else it - // will be GC'd at the end of this scope. The downside is - // that, unless we can decrement the ref when the unique_ptr - // is destroyed, then the object will live forever. - obj.inc_ref(); - - if (py::isinstance(obj)) - { - return std::unique_ptr(obj.cast()); - } - else if (py::isinstance(obj)) - { - return std::unique_ptr(obj.cast()); - } - else - { - throw std::runtime_error("invalid node type of " + name); - } - }); + factory.registerBuilder(manifest, makeTreeNodeBuilderFn(type)); }) .def("create_tree_from_text", [](BehaviorTreeFactory& factory, const std::string& text) -> Tree { @@ -173,16 +164,23 @@ PYBIND11_MODULE(btpy_cpp, m) py::class_(m, "NodeConfig"); - py::class_(m, "SyncActionNode") + // Register the C++ type hierarchy so that we can refer to Python subclasses + // by their superclass ptr types in generic C++ code. + py::class_(m, "_TreeNode"); + py::class_(m, "_ActionNodeBase"); + py::class_(m, "_SyncActionNode"); + py::class_(m, "_StatefulActionNode"); + + py::class_(m, "SyncActionNode") .def(py::init()) - .def("get_input", &Py_getInput) - .def("set_output", &Py_setOutput) + .def("get_input", &Py_getInput) + .def("set_output", &Py_setOutput) .def("tick", &Py_SyncActionNode::tick); - py::class_(m, "StatefulActionNode") + py::class_(m, "StatefulActionNode") .def(py::init()) - .def("get_input", &Py_getInput) - .def("set_output", &Py_setOutput) + .def("get_input", &Py_getInput) + .def("set_output", &Py_setOutput) .def("on_start", &Py_StatefulActionNode::onStart) .def("on_running", &Py_StatefulActionNode::onRunning) .def("on_halted", &Py_StatefulActionNode::onHalted); From b22772ed77e374fafae5123dc0e79fbcfa63cb1d Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 10:55:49 -0600 Subject: [PATCH 07/43] Put generic methods on abstract base class. --- src/python_bindings.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index a956ca8c9..48e20bead 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -166,21 +166,19 @@ PYBIND11_MODULE(btpy_cpp, m) // Register the C++ type hierarchy so that we can refer to Python subclasses // by their superclass ptr types in generic C++ code. - py::class_(m, "_TreeNode"); + py::class_(m, "_TreeNode") + .def("get_input", &Py_getInput) + .def("set_output", &Py_setOutput); py::class_(m, "_ActionNodeBase"); py::class_(m, "_SyncActionNode"); py::class_(m, "_StatefulActionNode"); py::class_(m, "SyncActionNode") .def(py::init()) - .def("get_input", &Py_getInput) - .def("set_output", &Py_setOutput) .def("tick", &Py_SyncActionNode::tick); py::class_(m, "StatefulActionNode") .def(py::init()) - .def("get_input", &Py_getInput) - .def("set_output", &Py_setOutput) .def("on_start", &Py_StatefulActionNode::onStart) .def("on_running", &Py_StatefulActionNode::onRunning) .def("on_halted", &Py_StatefulActionNode::onHalted); From 6188c215c2601233316ad0288f13a7e9ac1bd4d6 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 11:10:10 -0600 Subject: [PATCH 08/43] Ignore pycache. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2a13ff4e6..bc0cb7e33 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build* site/* /.vscode/ .vs/ +__pycache__ # clangd cache /.cache/* From 9493f103a3b844abab2b38ed145bb1106ed1bdc4 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 11:56:23 -0600 Subject: [PATCH 09/43] Return None if blackboard value doesn't exist. --- python_examples/ex02_generic_data.py | 4 +++- python_examples/ex03_stateful_nodes.py | 7 +++---- src/python_bindings.cpp | 5 ++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/python_examples/ex02_generic_data.py b/python_examples/ex02_generic_data.py index 5af53acfd..0a6c2bf0e 100644 --- a/python_examples/ex02_generic_data.py +++ b/python_examples/ex02_generic_data.py @@ -64,7 +64,9 @@ def tick(self): @ports(inputs=["value"]) class Print(SyncActionNode): def tick(self): - print(self.get_input("value")) + value = self.get_input("value") + if value is not None: + print(value) return NodeStatus.SUCCESS diff --git a/python_examples/ex03_stateful_nodes.py b/python_examples/ex03_stateful_nodes.py index d646a8649..55a243b02 100644 --- a/python_examples/ex03_stateful_nodes.py +++ b/python_examples/ex03_stateful_nodes.py @@ -23,9 +23,6 @@ - - - @@ -63,7 +60,9 @@ def on_halted(self): @ports(inputs=["value"]) class Print(SyncActionNode): def tick(self): - print(self.get_input("value")) + value = self.get_input("value") + if value is not None: + print(value) return NodeStatus.SUCCESS diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index 48e20bead..21647bb9c 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -58,7 +58,10 @@ class Py_StatefulActionNode final : public StatefulActionNode py::object Py_getInput(const TreeNode& node, const std::string& name) { py::object obj; - node.getInput(name, obj); + if (!node.getInput(name, obj).has_value()) + { + return py::none(); + } return obj; } From 404a195b52bc0f004c10e3039171c8651a313c1b Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 11:56:38 -0600 Subject: [PATCH 10/43] Add builder args to be passed to node ctors. --- src/python_bindings.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index 21647bb9c..ade6608b9 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -108,10 +108,12 @@ PortsList extractPortsList(const py::type& type) return ports; } -NodeBuilder makeTreeNodeBuilderFn(const py::type& type) +NodeBuilder makeTreeNodeBuilderFn(const py::type& type, const py::args& args, + const py::kwargs& kwargs) { - return [type](const auto& name, const auto& config) -> auto { - py::object obj = type(name, config); + return [=](const auto& name, const auto& config) -> auto { + py::object obj; + obj = type(name, config, *args, **kwargs); // TODO: Increment the object's reference count or else it // will be GC'd at the end of this scope. The downside is @@ -135,7 +137,8 @@ PYBIND11_MODULE(btpy_cpp, m) py::class_(m, "BehaviorTreeFactory") .def(py::init()) .def("register", - [](BehaviorTreeFactory& factory, const py::type type) { + [](BehaviorTreeFactory& factory, const py::type type, const py::args& args, + const py::kwargs& kwargs) { const std::string name = type.attr("__name__").cast(); TreeNodeManifest manifest; @@ -144,7 +147,7 @@ PYBIND11_MODULE(btpy_cpp, m) manifest.ports = extractPortsList(type); manifest.description = ""; - factory.registerBuilder(manifest, makeTreeNodeBuilderFn(type)); + factory.registerBuilder(manifest, makeTreeNodeBuilderFn(type, args, kwargs)); }) .def("create_tree_from_text", [](BehaviorTreeFactory& factory, const std::string& text) -> Tree { From adf5cef73f2f75cede5a7ca0090297b1b8140ddc Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 11:56:52 -0600 Subject: [PATCH 11/43] Add ROS2 interop example. --- python_examples/ex04_ros_interop.py | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 python_examples/ex04_ros_interop.py diff --git a/python_examples/ex04_ros_interop.py b/python_examples/ex04_ros_interop.py new file mode 100644 index 000000000..d31adcfc0 --- /dev/null +++ b/python_examples/ex04_ros_interop.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +""" +Demonstration of stateful action nodes. + +To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` +variable. It is probably located in your build directory if you're building from +source. +""" + +import rclpy +from rclpy.node import Node +from tf2_ros.buffer import Buffer +from tf2_ros.transform_listener import TransformListener + +from btpy import ( + BehaviorTreeFactory, + StatefulActionNode, + SyncActionNode, + NodeStatus, + ports, +) + + +xml_text = """ + + + + + + + + + + + + +""" + + +@ports(inputs=["frame_id", "child_frame_id"], outputs=["tf"]) +class GetRosTransform(StatefulActionNode): + def __init__(self, name, config, node): + super().__init__(name, config) + + self.node = node + self.tf_buffer = Buffer() + self.tf_listener = TransformListener(self.tf_buffer, self.node) + + def on_start(self): + return NodeStatus.RUNNING + + def on_running(self): + frame_id = self.get_input("frame_id") + child_frame_id = self.get_input("child_frame_id") + + time = self.node.get_clock().now() + if self.tf_buffer.can_transform(frame_id, child_frame_id, time): + tf = self.tf_buffer.lookup_transform(frame_id, child_frame_id, time) + self.set_output("tf", tf) + + return NodeStatus.RUNNING + + def on_halted(self): + pass + + +@ports(inputs=["value"]) +class Print(SyncActionNode): + def tick(self): + value = self.get_input("value") + if value is not None: + print(value) + return NodeStatus.SUCCESS + + +rclpy.init() +node = Node("ex04_ros_interopt") + +factory = BehaviorTreeFactory() +factory.register(GetRosTransform, node) +factory.register(Print) + +tree = factory.create_tree_from_text(xml_text) + +node.create_timer(0.01, lambda: tree.tick_once()) +rclpy.spin(node) From 37ae114cc6b3aff99ab6f28c4d0dfd6b1c2d1dde Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 12:01:52 -0600 Subject: [PATCH 12/43] Add simple README. --- python_examples/README.md | 4 ++++ python_examples/ex01_sample.py | 8 +------- python_examples/ex02_generic_data.py | 4 ---- python_examples/ex03_stateful_nodes.py | 4 ---- python_examples/ex04_ros_interop.py | 6 +----- 5 files changed, 6 insertions(+), 20 deletions(-) create mode 100644 python_examples/README.md diff --git a/python_examples/README.md b/python_examples/README.md new file mode 100644 index 000000000..e6511edd5 --- /dev/null +++ b/python_examples/README.md @@ -0,0 +1,4 @@ +1. Ensure that BehaviorTree.CPP is build with `BTCPP_PYTHON=ON`. +2. Add the build directory containing the `btpy_cpp.*.so` Python extension to + your `PYTHONPATH`. +3. Run an example, e.g. `python3 ex01_sample.py` diff --git a/python_examples/ex01_sample.py b/python_examples/ex01_sample.py index 3a89c4136..2eb0eda36 100644 --- a/python_examples/ex01_sample.py +++ b/python_examples/ex01_sample.py @@ -1,13 +1,7 @@ #!/usr/bin/env python3 """ -Demo adapted from [1]. - -To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` -variable. It is probably located in your build directory if you're building from -source. - -[1]: https://github.com/BehaviorTree/btcpp_sample +Demo adapted from [btcpp_sample](https://github.com/BehaviorTree/btcpp_sample). """ from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports diff --git a/python_examples/ex02_generic_data.py b/python_examples/ex02_generic_data.py index 0a6c2bf0e..e7d8c516b 100644 --- a/python_examples/ex02_generic_data.py +++ b/python_examples/ex02_generic_data.py @@ -2,10 +2,6 @@ """ Demonstration of passing generic data between nodes. - -To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` -variable. It is probably located in your build directory if you're building from -source. """ import numpy as np diff --git a/python_examples/ex03_stateful_nodes.py b/python_examples/ex03_stateful_nodes.py index 55a243b02..d89b9b00d 100644 --- a/python_examples/ex03_stateful_nodes.py +++ b/python_examples/ex03_stateful_nodes.py @@ -2,10 +2,6 @@ """ Demonstration of stateful action nodes. - -To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` -variable. It is probably located in your build directory if you're building from -source. """ import numpy as np diff --git a/python_examples/ex04_ros_interop.py b/python_examples/ex04_ros_interop.py index d31adcfc0..0eea5b20e 100644 --- a/python_examples/ex04_ros_interop.py +++ b/python_examples/ex04_ros_interop.py @@ -1,11 +1,7 @@ #!/usr/bin/env python3 """ -Demonstration of stateful action nodes. - -To run, ensure that the `btpy_cpp` Python extension is on your `PYTHONPATH` -variable. It is probably located in your build directory if you're building from -source. +Demonstrates interop of BehaviorTree.CPP Python bindings and ROS2 via rclpy. """ import rclpy From 2a22fa8490f3a3c5783efacd6508d2e6c0f01ec4 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 12:05:05 -0600 Subject: [PATCH 13/43] Add note about Py_getInput return value. --- src/python_bindings.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index ade6608b9..d32d22228 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -58,6 +58,9 @@ class Py_StatefulActionNode final : public StatefulActionNode py::object Py_getInput(const TreeNode& node, const std::string& name) { py::object obj; + + // The input could not exist on the blackboard, in which case we return Python + // `None` instead of an invalid object. if (!node.getInput(name, obj).has_value()) { return py::none(); From ed78e8450978abbe5d76044b08b8d46b40285520 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 12:27:34 -0600 Subject: [PATCH 14/43] Fix NodeStatus enum value ordering. --- src/python_bindings.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index d32d22228..694312a11 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -164,10 +164,10 @@ PYBIND11_MODULE(btpy_cpp, m) py::arg("sleep_time") = std::chrono::milliseconds(10)); py::enum_(m, "NodeStatus") - .value("SUCCESS", NodeStatus::SUCCESS) - .value("FAILURE", NodeStatus::FAILURE) .value("IDLE", NodeStatus::IDLE) .value("RUNNING", NodeStatus::RUNNING) + .value("SUCCESS", NodeStatus::SUCCESS) + .value("FAILURE", NodeStatus::FAILURE) .value("SKIPPED", NodeStatus::SKIPPED) .export_values(); From dc9e9535a4442bb3c7fadd01924c55cd8f14e07e Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 12:28:16 -0600 Subject: [PATCH 15/43] Fix typo in ex04. --- python_examples/ex04_ros_interop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_examples/ex04_ros_interop.py b/python_examples/ex04_ros_interop.py index 0eea5b20e..56a6daf13 100644 --- a/python_examples/ex04_ros_interop.py +++ b/python_examples/ex04_ros_interop.py @@ -71,7 +71,7 @@ def tick(self): rclpy.init() -node = Node("ex04_ros_interopt") +node = Node("ex04_ros_interop") factory = BehaviorTreeFactory() factory.register(GetRosTransform, node) From 712370bac554b665330791720d01869a88f8acd0 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 13 Aug 2023 12:30:01 -0600 Subject: [PATCH 16/43] Add useful command for ex04. --- python_examples/ex04_ros_interop.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python_examples/ex04_ros_interop.py b/python_examples/ex04_ros_interop.py index 56a6daf13..58a2f8958 100644 --- a/python_examples/ex04_ros_interop.py +++ b/python_examples/ex04_ros_interop.py @@ -2,6 +2,12 @@ """ Demonstrates interop of BehaviorTree.CPP Python bindings and ROS2 via rclpy. + +You can publish the transform expected in the tree below using this command: + + ros2 run tf2_ros static_transform_publisher \ + --frame-id odom --child-frame-id base_link \ + --x 1.0 --y 2.0 """ import rclpy From 7927e67e0dd3c2c07a29d997bea98436277c5cbc Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 19 Aug 2023 12:51:44 -0600 Subject: [PATCH 17/43] Fix onHalted override copy-paste error. --- src/python_bindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index 694312a11..d1305271a 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -51,7 +51,7 @@ class Py_StatefulActionNode final : public StatefulActionNode void onHalted() override { - PYBIND11_OVERRIDE_PURE_NAME(void, Py_StatefulActionNode, "on_running", onRunning); + PYBIND11_OVERRIDE_PURE_NAME(void, Py_StatefulActionNode, "on_halted", onHalted); } }; From cfa553a32af0f9aa8f9465590efb9f049cfd6826 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 19 Aug 2023 12:52:01 -0600 Subject: [PATCH 18/43] Disable zero variadic arg warning. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 313ad50f5..77e059755 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,6 +139,7 @@ if(BTCPP_PYTHON) find_package(Python COMPONENTS Interpreter Development) find_package(pybind11 CONFIG) pybind11_add_module(btpy_cpp src/python_bindings.cpp) + target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments) target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY}) endif() From d1fe0e36027691feaf90c41d533ef86d51115f36 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 19 Aug 2023 18:52:39 -0600 Subject: [PATCH 19/43] Implement C++ <-> Python type interop via JSON. --- CMakeLists.txt | 18 +- .../contrib/pybind11_json.hpp | 226 ++++++++++++++++++ include/behaviortree_cpp/json_export.h | 21 +- include/behaviortree_cpp/python_types.h | 39 +++ include/behaviortree_cpp/tree_node.h | 33 ++- python_examples/ex05_type_interop.py | 63 +++++ sample_nodes/dummy_nodes.h | 79 ++++++ src/json_export.cpp | 9 +- src/python_bindings.cpp | 4 +- src/python_types.cpp | 25 ++ 10 files changed, 501 insertions(+), 16 deletions(-) create mode 100644 include/behaviortree_cpp/contrib/pybind11_json.hpp create mode 100644 include/behaviortree_cpp/python_types.h create mode 100644 python_examples/ex05_type_interop.py create mode 100644 src/python_types.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 77e059755..631cf80ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,11 +136,7 @@ if(BTCPP_SQLITE_LOGGING) endif() if(BTCPP_PYTHON) - find_package(Python COMPONENTS Interpreter Development) - find_package(pybind11 CONFIG) - pybind11_add_module(btpy_cpp src/python_bindings.cpp) - target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments) - target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY}) + list(APPEND BT_SOURCE src/python_types.cpp) endif() ###################################################### @@ -172,6 +168,18 @@ target_link_libraries(${BTCPP_LIBRARY} ${BTCPP_EXTRA_LIBRARIES} ) +if(BTCPP_PYTHON) + find_package(Python COMPONENTS Interpreter Development) + find_package(pybind11 CONFIG) + + pybind11_add_module(btpy_cpp src/python_types.cpp src/python_bindings.cpp) + target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments) + target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY}) + + target_link_libraries(${BTCPP_LIBRARY} PUBLIC Python::Python pybind11::pybind11) + target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_PYTHON) +endif() + target_include_directories(${BTCPP_LIBRARY} PUBLIC $ diff --git a/include/behaviortree_cpp/contrib/pybind11_json.hpp b/include/behaviortree_cpp/contrib/pybind11_json.hpp new file mode 100644 index 000000000..fe76b57e7 --- /dev/null +++ b/include/behaviortree_cpp/contrib/pybind11_json.hpp @@ -0,0 +1,226 @@ +/*************************************************************************** +* Copyright (c) 2019, Martin Renou * +* * +* Distributed under the terms of the BSD 3-Clause License. * +* * +* The full license is in the file LICENSE, distributed with this software. * +****************************************************************************/ + +#ifndef PYBIND11_JSON_HPP +#define PYBIND11_JSON_HPP + +#include +#include + +#include "behaviortree_cpp/contrib/json.hpp" + +#include "pybind11/pybind11.h" + +namespace pyjson +{ + namespace py = pybind11; + namespace nl = nlohmann; + + inline py::object from_json(const nl::json& j) + { + if (j.is_null()) + { + return py::none(); + } + else if (j.is_boolean()) + { + return py::bool_(j.get()); + } + else if (j.is_number_unsigned()) + { + return py::int_(j.get()); + } + else if (j.is_number_integer()) + { + return py::int_(j.get()); + } + else if (j.is_number_float()) + { + return py::float_(j.get()); + } + else if (j.is_string()) + { + return py::str(j.get()); + } + else if (j.is_array()) + { + py::list obj(j.size()); + for (std::size_t i = 0; i < j.size(); i++) + { + obj[i] = from_json(j[i]); + } + return obj; + } + else // Object + { + py::dict obj; + for (nl::json::const_iterator it = j.cbegin(); it != j.cend(); ++it) + { + obj[py::str(it.key())] = from_json(it.value()); + } + return obj; + } + } + + inline nl::json to_json(const py::handle& obj) + { + if (obj.ptr() == nullptr || obj.is_none()) + { + return nullptr; + } + if (py::isinstance(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj)) + { + try + { + nl::json::number_integer_t s = obj.cast(); + if (py::int_(s).equal(obj)) + { + return s; + } + } + catch (...) + { + } + try + { + nl::json::number_unsigned_t u = obj.cast(); + if (py::int_(u).equal(obj)) + { + return u; + } + } + catch (...) + { + } + throw std::runtime_error("to_json received an integer out of range for both nl::json::number_integer_t and nl::json::number_unsigned_t type: " + py::repr(obj).cast()); + } + if (py::isinstance(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj)) + { + py::module base64 = py::module::import("base64"); + return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast(); + } + if (py::isinstance(obj)) + { + return obj.cast(); + } + if (py::isinstance(obj) || py::isinstance(obj)) + { + auto out = nl::json::array(); + for (const py::handle value : obj) + { + out.push_back(to_json(value)); + } + return out; + } + if (py::isinstance(obj)) + { + auto out = nl::json::object(); + for (const py::handle key : obj) + { + out[py::str(key).cast()] = to_json(obj[key]); + } + return out; + } + throw std::runtime_error("to_json not implemented for this type of object: " + py::repr(obj).cast()); + } +} + +// nlohmann_json serializers +namespace nlohmann +{ + namespace py = pybind11; + + #define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \ + template <> \ + struct adl_serializer \ + { \ + inline static void to_json(json& j, const T& obj) \ + { \ + j = pyjson::to_json(obj); \ + } \ + \ + inline static T from_json(const json& j) \ + { \ + return pyjson::from_json(j); \ + } \ + } + + #define MAKE_NLJSON_SERIALIZER_ONLY(T) \ + template <> \ + struct adl_serializer \ + { \ + inline static void to_json(json& j, const T& obj) \ + { \ + j = pyjson::to_json(obj); \ + } \ + } + + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::object); + + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::bool_); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::int_); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::float_); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::str); + + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::list); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::tuple); + MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::dict); + + MAKE_NLJSON_SERIALIZER_ONLY(py::handle); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::item_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::list_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::tuple_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::sequence_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::str_attr_accessor); + MAKE_NLJSON_SERIALIZER_ONLY(py::detail::obj_attr_accessor); + + #undef MAKE_NLJSON_SERIALIZER + #undef MAKE_NLJSON_SERIALIZER_ONLY +} + +// pybind11 caster +namespace pybind11 +{ + namespace detail + { + template <> struct type_caster + { + public: + PYBIND11_TYPE_CASTER(nlohmann::json, _("json")); + + bool load(handle src, bool) + { + try + { + value = pyjson::to_json(src); + return true; + } + catch (...) + { + return false; + } + } + + static handle cast(nlohmann::json src, return_value_policy /* policy */, handle /* parent */) + { + object obj = pyjson::from_json(src); + return obj.release(); + } + }; + } +} + +#endif diff --git a/include/behaviortree_cpp/json_export.h b/include/behaviortree_cpp/json_export.h index 8fc6d2c25..b6c35644c 100644 --- a/include/behaviortree_cpp/json_export.h +++ b/include/behaviortree_cpp/json_export.h @@ -23,12 +23,8 @@ namespace BT */ class JsonExporter{ - - public: - static JsonExporter& get() { - static JsonExporter global_instance; - return global_instance; - } +public: + static JsonExporter& get(); /** * @brief toJson adds the content of "any" to the JSON "destination". @@ -43,6 +39,19 @@ class JsonExporter{ dst = val; } + template + T fromJson(const nlohmann::json& src) const + { + // TODO: Implement a similar "converter" interface as toJson. + return src.template get(); + } + + template + void fromJson(const nlohmann::json& src, T& dst) const + { + dst = fromJson(src); + } + /// Register new JSON converters with addConverter(), /// But works only if this function is implemented: /// diff --git a/include/behaviortree_cpp/python_types.h b/include/behaviortree_cpp/python_types.h new file mode 100644 index 000000000..bcd53a7e3 --- /dev/null +++ b/include/behaviortree_cpp/python_types.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "behaviortree_cpp/json_export.h" +#include "behaviortree_cpp/contrib/json.hpp" +#include "behaviortree_cpp/contrib/pybind11_json.hpp" +#include "behaviortree_cpp/utils/safe_any.hpp" + +namespace BT +{ + +/** + * @brief Generic method to convert Python objects to type T via JSON. + * + * For this function to succeed, the type T must be convertible from JSON via + * the JsonExporter interface. + */ +template +bool fromPythonObject(const pybind11::object& obj, T& dest) +{ + if constexpr (nlohmann::detail::is_getable::value) + { + JsonExporter::get().fromJson(obj, dest); + return true; + } + + return false; +} + +/** + * @brief Convert a BT::Any to a Python object via JSON. + * + * For this function to succeed, the type stored inside the Any must be + * convertible to JSON via the JsonExporter interface. + */ +bool toPythonObject(const BT::Any& val, pybind11::object& dest); + +} // namespace BT diff --git a/include/behaviortree_cpp/tree_node.h b/include/behaviortree_cpp/tree_node.h index 4dc44405f..339b73feb 100644 --- a/include/behaviortree_cpp/tree_node.h +++ b/include/behaviortree_cpp/tree_node.h @@ -25,6 +25,13 @@ #include "behaviortree_cpp/utils/wakeup_signal.hpp" #include "behaviortree_cpp/scripting/script_parser.hpp" +#ifdef BTCPP_PYTHON +#include +#include + +#include "behaviortree_cpp/python_types.h" +#endif + #ifdef _MSC_VER #pragma warning(disable : 4127) #endif @@ -442,11 +449,33 @@ inline Result TreeNode::getInput(const std::string& key, T& destination) const auto val = any_ref.get(); if(!val->empty()) { - if (!std::is_same_v && - val->type() == typeid(std::string)) + // Trivial conversion (T -> T) + if (val->type() == typeid(T)) + { + destination = val->cast(); + } + else if (!std::is_same_v && val->type() == typeid(std::string)) { destination = ParseString(val->cast()); } +#ifdef BTCPP_PYTHON + // py::object -> C++ + else if (val->type() == typeid(pybind11::object)) + { + if (!fromPythonObject(val->cast(), destination)) + { + return nonstd::make_unexpected("Cannot convert from Python object"); + } + } + // C++ -> py::object + else if constexpr (std::is_same_v) + { + if (!toPythonObject(*val, destination)) + { + return nonstd::make_unexpected("Cannot convert to Python object"); + } + } +#endif else { destination = val->cast(); diff --git a/python_examples/ex05_type_interop.py b/python_examples/ex05_type_interop.py new file mode 100644 index 000000000..6b39b30e8 --- /dev/null +++ b/python_examples/ex05_type_interop.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +""" +Demo of seamless conversion between C++ and Python types. +""" + +from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports + + +xml_text = """ + + + + + + + + + + + + + + + + + + + +""" + + +@ports(outputs=["output"]) +class PutVector(SyncActionNode): + def tick(self): + # Schema matching std::unordered_map + # (defined in dummy_nodes.h, input type of PrintComplex) + self.set_output( + "output", + { + "a": {"x": 0.0, "y": 42.0, "z": 9.0}, + "b": {"x": 1.0, "y": -2.0, "z": 1.0}, + }, + ) + return NodeStatus.SUCCESS + + +@ports(inputs=["value"]) +class Print(SyncActionNode): + def tick(self): + value = self.get_input("value") + if value is not None: + print("Python:", value) + return NodeStatus.SUCCESS + + +factory = BehaviorTreeFactory() +factory.register_from_plugin("sample_nodes/bin/libdummy_nodes_dyn.so") +factory.register(PutVector) +factory.register(Print) + +tree = factory.create_tree_from_text(xml_text) +tree.tick_while_running() diff --git a/sample_nodes/dummy_nodes.h b/sample_nodes/dummy_nodes.h index c0b1df87c..71ed3d3d6 100644 --- a/sample_nodes/dummy_nodes.h +++ b/sample_nodes/dummy_nodes.h @@ -3,6 +3,7 @@ #include "behaviortree_cpp/behavior_tree.h" #include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp/json_export.h" namespace DummyNodes { @@ -121,6 +122,80 @@ class SleepNode : public BT::StatefulActionNode std::chrono::system_clock::time_point deadline_; }; +struct Vector3 +{ + float x; + float y; + float z; +}; + +void to_json(nlohmann::json& j, const Vector3& p) +{ + j = nlohmann::json{{"x", p.x}, {"y", p.y}, {"z", p.z}}; +} + +void from_json(const nlohmann::json& j, Vector3& p) +{ + j.at("x").get_to(p.x); + j.at("y").get_to(p.y); + j.at("z").get_to(p.z); +} + +class RandomVector : public BT::SyncActionNode +{ +public: + RandomVector(const std::string& name, const BT::NodeConfig& config) : + BT::SyncActionNode(name, config) + {} + + // You must override the virtual function tick() + NodeStatus tick() override + { + setOutput("vector", Vector3{1.0, 2.0, 3.0}); + return BT::NodeStatus::SUCCESS; + } + + // It is mandatory to define this static method. + static BT::PortsList providedPorts() + { + return {BT::OutputPort("vector")}; + } +}; + +class PrintComplex : public BT::SyncActionNode +{ +public: + PrintComplex(const std::string& name, const BT::NodeConfig& config) : + BT::SyncActionNode(name, config) + {} + + // You must override the virtual function tick() + NodeStatus tick() override + { + auto input = getInput>("input"); + if (input.has_value()) + { + std::cerr << "C++: {"; + for (const auto& [key, value] : *input) + { + std::cerr << key << ": (" + << value.x << ", " + << value.y << ", " + << value.z << "), "; + } + std::cerr << "}" << std::endl;; + } + + return BT::NodeStatus::SUCCESS; + } + + // It is mandatory to define this static method. + static BT::PortsList providedPorts() + { + return {BT::InputPort>("input")}; + } +}; + inline void RegisterNodes(BT::BehaviorTreeFactory& factory) { static GripperInterface grip_singleton; @@ -132,6 +207,10 @@ inline void RegisterNodes(BT::BehaviorTreeFactory& factory) factory.registerSimpleAction("CloseGripper", std::bind(&GripperInterface::close, &grip_singleton)); factory.registerNodeType("ApproachObject"); factory.registerNodeType("SaySomething"); + factory.registerNodeType("RandomVector"); + factory.registerNodeType("PrintComplex"); + + BT::JsonExporter::get().addConverter(); } } // end namespace diff --git a/src/json_export.cpp b/src/json_export.cpp index 41084e914..766dac314 100644 --- a/src/json_export.cpp +++ b/src/json_export.cpp @@ -3,7 +3,14 @@ namespace BT { -bool JsonExporter::toJson(const Any &any, nlohmann::json &dst) const +static JsonExporter global_instance; + +JsonExporter& JsonExporter::get() +{ + return global_instance; +} + +bool JsonExporter::toJson(const Any& any, nlohmann::json& dst) const { nlohmann::json json; auto const& type = any.castedType(); diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index d1305271a..da60b6e9e 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -99,13 +99,13 @@ PortsList extractPortsList(const py::type& type) const auto input_ports = type.attr("input_ports").cast(); for (const auto& name : input_ports) { - ports.insert(InputPort(name.cast())); + ports.insert(InputPort(name.cast())); } const auto output_ports = type.attr("output_ports").cast(); for (const auto& name : output_ports) { - ports.insert(OutputPort(name.cast())); + ports.insert(OutputPort(name.cast())); } return ports; diff --git a/src/python_types.cpp b/src/python_types.cpp new file mode 100644 index 000000000..e41d57f80 --- /dev/null +++ b/src/python_types.cpp @@ -0,0 +1,25 @@ +#include "behaviortree_cpp/python_types.h" + +#include +#include + +#include "behaviortree_cpp/json_export.h" +#include "behaviortree_cpp/contrib/json.hpp" +#include "behaviortree_cpp/contrib/pybind11_json.hpp" + +namespace BT +{ + +bool toPythonObject(const BT::Any& val, pybind11::object& dest) +{ + nlohmann::json json; + if (JsonExporter::get().toJson(val, json)) + { + dest = json; + return true; + } + + return false; +} + +} // namespace BT From 21d450e880e772c4e3e8a11d865a19d07ce5270d Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 19 Aug 2023 18:52:52 -0600 Subject: [PATCH 20/43] Add `BehaviorTreeFactory.register_from_plugin` binding. --- src/python_bindings.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index da60b6e9e..fcb6223f1 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -152,6 +152,7 @@ PYBIND11_MODULE(btpy_cpp, m) factory.registerBuilder(manifest, makeTreeNodeBuilderFn(type, args, kwargs)); }) + .def("register_from_plugin", &BehaviorTreeFactory::registerFromPlugin) .def("create_tree_from_text", [](BehaviorTreeFactory& factory, const std::string& text) -> Tree { return factory.createTreeFromText(text); From 44483766436f16effbb91cb6b7559be387b35193 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 19 Aug 2023 19:07:54 -0600 Subject: [PATCH 21/43] Add pybind11 conan dependency. --- conanfile.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/conanfile.txt b/conanfile.txt index 56dd97006..0f2e22339 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -2,6 +2,7 @@ gtest/1.12.1 zeromq/4.3.4 sqlite3/3.40.1 +pybind11/2.10.4 [generators] CMakeDeps From c703efd9d570caad46eac5ed2fc06bc63c39f6d5 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 19 Aug 2023 19:49:01 -0600 Subject: [PATCH 22/43] Implement coroutine-based Python nodes. --- python_examples/btpy.py | 41 ++++++++++++++++++ python_examples/ex06_async_nodes.py | 67 +++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 python_examples/ex06_async_nodes.py diff --git a/python_examples/btpy.py b/python_examples/btpy.py index 469e1bc7c..514f1c0b9 100644 --- a/python_examples/btpy.py +++ b/python_examples/btpy.py @@ -23,3 +23,44 @@ def specify_ports(cls): return cls return specify_ports + + +class AsyncActionNode(StatefulActionNode): + """An abstract action node implemented via cooperative multitasking. + + Subclasses must implement the `run()` method as a generator. Optionally, + this method can return a final `NodeStatus` value to indicate its exit + condition. + + Note: + It is the responsibility of the action author to not block the main + behavior tree loop with long-running tasks. `yield` calls should be + placed whenever a pause is appropriate. + """ + + def __init__(self, name, config): + super().__init__(name, config) + + def on_start(self): + self.coroutine = self.run() + return NodeStatus.RUNNING + + def on_running(self): + # The library logic should never allow this to happen, but users can + # still manually call `on_running` without an associated `on_start` + # call. Make sure to print a useful error when this happens. + if self.coroutine is None: + raise "AsyncActionNode run without starting" + + # Resume the coroutine (generator). As long as the generator is not + # exhausted, keep this action in the RUNNING state. + try: + next(self.coroutine) + return NodeStatus.RUNNING + except StopIteration as e: + # If the action returns a status then propagate it upwards. + if e.value is not None: + return e.value + # Otherwise, just assume the action finished successfully. + else: + return NodeStatus.SUCCESS diff --git a/python_examples/ex06_async_nodes.py b/python_examples/ex06_async_nodes.py new file mode 100644 index 000000000..609e2dcdc --- /dev/null +++ b/python_examples/ex06_async_nodes.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +""" +Demonstration of an asynchronous action node implemented conveniently as a +Python coroutine. This enables simple synchronous code to be written in place of +complex asynchronous state machines. +""" + +import time +import numpy as np +from btpy import ( + AsyncActionNode, + BehaviorTreeFactory, + SyncActionNode, + NodeStatus, + ports, +) + + +xml_text = """ + + + + + + + + + + +""" + + +@ports(inputs=["start", "goal"], outputs=["command"]) +class MyAsyncNode(AsyncActionNode): + def run(self): + start = np.asarray(self.get_input("start")) + goal = np.asarray(self.get_input("goal")) + + for t in np.linspace(0.0, 1.0, num=10): + command = (1.0 - t) * start + t * goal + self.set_output("command", command) + yield + + print("Trajectory finished!") + + return NodeStatus.SUCCESS + + def on_halted(self): + print("Aborted") + + +@ports(inputs=["value"]) +class Print(SyncActionNode): + def tick(self): + value = self.get_input("value") + if value is not None: + print(value) + return NodeStatus.SUCCESS + + +factory = BehaviorTreeFactory() +factory.register(MyAsyncNode) +factory.register(Print) + +tree = factory.create_tree_from_text(xml_text) +tree.tick_while_running() From 83caef78141e625947e253ecc3ccbdf9a8959bbf Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Fri, 1 Sep 2023 19:35:46 -0600 Subject: [PATCH 23/43] Add missing pybind11 dependency to package.xml --- package.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/package.xml b/package.xml index 5abad954d..2530a1f35 100644 --- a/package.xml +++ b/package.xml @@ -23,6 +23,7 @@ libsqlite3-dev libzmq3-dev + pybind11-dev ament_cmake_gtest From 0e35ac06a5a84e61ee46690f263653ab0fbdf51b Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Fri, 1 Sep 2023 19:35:57 -0600 Subject: [PATCH 24/43] Move some dummy_nodes definitions to cpp file to fix linker error --- sample_nodes/dummy_nodes.cpp | 12 ++++++++++++ sample_nodes/dummy_nodes.h | 12 ++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/sample_nodes/dummy_nodes.cpp b/sample_nodes/dummy_nodes.cpp index 9c0a228f5..347996c4f 100644 --- a/sample_nodes/dummy_nodes.cpp +++ b/sample_nodes/dummy_nodes.cpp @@ -72,4 +72,16 @@ BT::NodeStatus SaySomethingSimple(BT::TreeNode &self) return BT::NodeStatus::SUCCESS; } +void to_json(nlohmann::json& j, const Vector3& p) +{ + j = nlohmann::json{{"x", p.x}, {"y", p.y}, {"z", p.z}}; +} + +void from_json(const nlohmann::json& j, Vector3& p) +{ + j.at("x").get_to(p.x); + j.at("y").get_to(p.y); + j.at("z").get_to(p.z); +} + } diff --git a/sample_nodes/dummy_nodes.h b/sample_nodes/dummy_nodes.h index 71ed3d3d6..0aaa951ed 100644 --- a/sample_nodes/dummy_nodes.h +++ b/sample_nodes/dummy_nodes.h @@ -129,17 +129,9 @@ struct Vector3 float z; }; -void to_json(nlohmann::json& j, const Vector3& p) -{ - j = nlohmann::json{{"x", p.x}, {"y", p.y}, {"z", p.z}}; -} +void to_json(nlohmann::json& j, const Vector3& p); -void from_json(const nlohmann::json& j, Vector3& p) -{ - j.at("x").get_to(p.x); - j.at("y").get_to(p.y); - j.at("z").get_to(p.z); -} +void from_json(const nlohmann::json& j, Vector3& p); class RandomVector : public BT::SyncActionNode { From 1a69d3ae1922be76829de417ef8ee7dc7aca5b80 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 26 Aug 2023 22:47:56 -0600 Subject: [PATCH 25/43] Clean up Python ex06. --- python_examples/ex06_async_nodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python_examples/ex06_async_nodes.py b/python_examples/ex06_async_nodes.py index 609e2dcdc..35623295f 100644 --- a/python_examples/ex06_async_nodes.py +++ b/python_examples/ex06_async_nodes.py @@ -37,13 +37,13 @@ def run(self): start = np.asarray(self.get_input("start")) goal = np.asarray(self.get_input("goal")) - for t in np.linspace(0.0, 1.0, num=10): + t0 = time.time() + while (t := time.time() - t0) < 1.0: command = (1.0 - t) * start + t * goal self.set_output("command", command) yield print("Trajectory finished!") - return NodeStatus.SUCCESS def on_halted(self): From 2c1b18a33bf096c1e6aa4a3d32739c34341459dd Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Fri, 1 Sep 2023 21:40:14 -0600 Subject: [PATCH 26/43] Use docstring as tree node description. --- src/python_bindings.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index fcb6223f1..a8d0b4962 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -150,6 +150,12 @@ PYBIND11_MODULE(btpy_cpp, m) manifest.ports = extractPortsList(type); manifest.description = ""; + // Use the type's docstring as the node description, if it exists. + if (const auto doc = type.attr("__doc__"); !doc.is_none()) + { + manifest.description = doc.cast(); + } + factory.registerBuilder(manifest, makeTreeNodeBuilderFn(type, args, kwargs)); }) .def("register_from_plugin", &BehaviorTreeFactory::registerFromPlugin) From fdc2232fe1b2fe7faa245bc3de2301ad2153e283 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Fri, 1 Sep 2023 22:18:04 -0600 Subject: [PATCH 27/43] Add pyproject.toml/setup.py for building wheels. --- python_examples/btpy.py => btpy/__init__.py | 0 pyproject.toml | 8 ++ python_examples/README.md | 6 +- setup.py | 141 ++++++++++++++++++++ 4 files changed, 151 insertions(+), 4 deletions(-) rename python_examples/btpy.py => btpy/__init__.py (100%) create mode 100644 pyproject.toml create mode 100644 setup.py diff --git a/python_examples/btpy.py b/btpy/__init__.py similarity index 100% rename from python_examples/btpy.py rename to btpy/__init__.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..b07da1aa8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", + "ninja", + "cmake>=3.16", +] +build-backend = "setuptools.build_meta" diff --git a/python_examples/README.md b/python_examples/README.md index e6511edd5..8e9ec0f31 100644 --- a/python_examples/README.md +++ b/python_examples/README.md @@ -1,4 +1,2 @@ -1. Ensure that BehaviorTree.CPP is build with `BTCPP_PYTHON=ON`. -2. Add the build directory containing the `btpy_cpp.*.so` Python extension to - your `PYTHONPATH`. -3. Run an example, e.g. `python3 ex01_sample.py` +1. Install the bindings by running `pip install .` from the project root. +2. Run an example, e.g. `python3 ex01_sample.py` diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..1092583a1 --- /dev/null +++ b/setup.py @@ -0,0 +1,141 @@ +import os +import re +import subprocess +import sys +from pathlib import Path + +from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext + +# Convert distutils Windows platform specifiers to CMake -A arguments +PLAT_TO_CMAKE = { + "win32": "Win32", + "win-amd64": "x64", + "win-arm32": "ARM", + "win-arm64": "ARM64", +} + + +# A CMakeExtension needs a sourcedir instead of a file list. +# The name must be the _single_ output extension from the CMake build. +# If you need multiple extensions, see scikit-build. +class CMakeExtension(Extension): + def __init__(self, name: str, sourcedir: str = "") -> None: + super().__init__(name, sources=[]) + self.sourcedir = os.fspath(Path(sourcedir).resolve()) + + +class CMakeBuild(build_ext): + def build_extension(self, ext: CMakeExtension) -> None: + # Must be in this form due to bug in .resolve() only fixed in Python 3.10+ + ext_fullpath = Path.cwd() / self.get_ext_fullpath(ext.name) + extdir = ext_fullpath.parent.resolve() + + # Using this requires trailing slash for auto-detection & inclusion of + # auxiliary "native" libs + + debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug + cfg = "Debug" if debug else "Release" + + # CMake lets you override the generator - we need to check this. + # Can be set with Conda-Build, for example. + cmake_generator = os.environ.get("CMAKE_GENERATOR", "") + + # Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON + # EXAMPLE_VERSION_INFO shows you how to pass a value into the C++ code + # from Python. + cmake_args = [ + f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}", + f"-DPYTHON_EXECUTABLE={sys.executable}", + f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm + # BehaviorTree.CPP specific CMake options + "-DBTCPP_BUILD_TOOLS=OFF", + "-DBTCPP_EXAMPLES=OFF", + "-DBTCPP_UNIT_TESTS=OFF", + ] + build_args = [] + # Adding CMake arguments set as environment variable + # (needed e.g. to build for ARM OSx on conda-forge) + if "CMAKE_ARGS" in os.environ: + cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item] + + if self.compiler.compiler_type != "msvc": + # Using Ninja-build since it a) is available as a wheel and b) + # multithreads automatically. MSVC would require all variables be + # exported for Ninja to pick it up, which is a little tricky to do. + # Users can override the generator with CMAKE_GENERATOR in CMake + # 3.15+. + if not cmake_generator or cmake_generator == "Ninja": + try: + import ninja + + ninja_executable_path = Path(ninja.BIN_DIR) / "ninja" + cmake_args += [ + "-GNinja", + f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}", + ] + except ImportError: + pass + + else: + # Single config generators are handled "normally" + single_config = any(x in cmake_generator for x in {"NMake", "Ninja"}) + + # CMake allows an arch-in-generator style for backward compatibility + contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"}) + + # Specify the arch if using MSVC generator, but only if it doesn't + # contain a backward-compatibility arch spec already in the + # generator name. + if not single_config and not contains_arch: + cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]] + + # Multi-config generators have a different way to specify configs + if not single_config: + cmake_args += [ + f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}" + ] + build_args += ["--config", cfg] + + if sys.platform.startswith("darwin"): + # Cross-compile support for macOS - respect ARCHFLAGS if set + archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", "")) + if archs: + cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))] + + # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level + # across all generators. + if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ: + # self.parallel is a Python 3 only way to set parallel jobs by hand + # using -j in the build_ext call, not supported by pip or PyPA-build. + if hasattr(self, "parallel") and self.parallel: + # CMake 3.12+ only. + build_args += [f"-j{self.parallel}"] + + build_temp = Path(self.build_temp) / ext.name + if not build_temp.exists(): + build_temp.mkdir(parents=True) + + subprocess.run( + ["cmake", ext.sourcedir, *cmake_args], cwd=build_temp, check=True + ) + subprocess.run( + ["cmake", "--build", ".", *build_args], cwd=build_temp, check=True + ) + + +# The information here can also be placed in setup.cfg - better separation of +# logic and declaration, and simpler if you include description/version in a file. +setup( + name="btcpp", + version="0.0.1", + author="Kyle Cesare", + author_email="kcesare@gmail.com", + description="Python bindings to the BehaviorTree.CPP project", + long_description="", + packages=["btpy"], + ext_modules=[CMakeExtension("btcpp")], + cmdclass={"build_ext": CMakeBuild}, + zip_safe=False, + python_requires=">=3.7", +) From ee7f464357171d7dbb2a57c5c3fa79c5dce15e25 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sat, 2 Sep 2023 11:08:42 -0600 Subject: [PATCH 28/43] Modify py::type argument to support older pybind --- src/python_bindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python_bindings.cpp b/src/python_bindings.cpp index a8d0b4962..b40053226 100644 --- a/src/python_bindings.cpp +++ b/src/python_bindings.cpp @@ -140,7 +140,7 @@ PYBIND11_MODULE(btpy_cpp, m) py::class_(m, "BehaviorTreeFactory") .def(py::init()) .def("register", - [](BehaviorTreeFactory& factory, const py::type type, const py::args& args, + [](BehaviorTreeFactory& factory, const py::object& type, const py::args& args, const py::kwargs& kwargs) { const std::string name = type.attr("__name__").cast(); From 9d8db3cc5ca56ccbba1e9fe71746f298830c18b5 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 3 Sep 2023 11:57:13 -0600 Subject: [PATCH 29/43] Clean up Python example XMLs. --- python_examples/ex03_stateful_nodes.py | 14 ++++++-------- python_examples/ex04_ros_interop.py | 10 ++++------ python_examples/ex05_type_interop.py | 26 +++++++++++++------------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/python_examples/ex03_stateful_nodes.py b/python_examples/ex03_stateful_nodes.py index d89b9b00d..aadca07f0 100644 --- a/python_examples/ex03_stateful_nodes.py +++ b/python_examples/ex03_stateful_nodes.py @@ -18,14 +18,12 @@ - - - - - - - + + + + + diff --git a/python_examples/ex04_ros_interop.py b/python_examples/ex04_ros_interop.py index 58a2f8958..78178b2dd 100644 --- a/python_examples/ex04_ros_interop.py +++ b/python_examples/ex04_ros_interop.py @@ -28,12 +28,10 @@ - - - - - - + + + + diff --git a/python_examples/ex05_type_interop.py b/python_examples/ex05_type_interop.py index 6b39b30e8..a322bce17 100644 --- a/python_examples/ex05_type_interop.py +++ b/python_examples/ex05_type_interop.py @@ -11,19 +11,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + From 46929a8f2b62a66c1dfd72eec9368675c38c3fe3 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 3 Sep 2023 12:31:00 -0600 Subject: [PATCH 30/43] Move Python-related source files into subdirectory. --- CMakeLists.txt | 4 ++-- include/behaviortree_cpp/{python_types.h => python/types.h} | 0 include/behaviortree_cpp/tree_node.h | 2 +- src/{python_bindings.cpp => python/bindings.cpp} | 0 src/{python_types.cpp => python/types.cpp} | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename include/behaviortree_cpp/{python_types.h => python/types.h} (100%) rename src/{python_bindings.cpp => python/bindings.cpp} (100%) rename src/{python_types.cpp => python/types.cpp} (91%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 631cf80ee..1edbc2656 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,7 +136,7 @@ if(BTCPP_SQLITE_LOGGING) endif() if(BTCPP_PYTHON) - list(APPEND BT_SOURCE src/python_types.cpp) + list(APPEND BT_SOURCE src/python/types.cpp) endif() ###################################################### @@ -172,7 +172,7 @@ if(BTCPP_PYTHON) find_package(Python COMPONENTS Interpreter Development) find_package(pybind11 CONFIG) - pybind11_add_module(btpy_cpp src/python_types.cpp src/python_bindings.cpp) + pybind11_add_module(btpy_cpp src/python/bindings.cpp) target_compile_options(btpy_cpp PRIVATE -Wno-gnu-zero-variadic-macro-arguments) target_link_libraries(btpy_cpp PRIVATE ${BTCPP_LIBRARY}) diff --git a/include/behaviortree_cpp/python_types.h b/include/behaviortree_cpp/python/types.h similarity index 100% rename from include/behaviortree_cpp/python_types.h rename to include/behaviortree_cpp/python/types.h diff --git a/include/behaviortree_cpp/tree_node.h b/include/behaviortree_cpp/tree_node.h index 339b73feb..e0aba462f 100644 --- a/include/behaviortree_cpp/tree_node.h +++ b/include/behaviortree_cpp/tree_node.h @@ -29,7 +29,7 @@ #include #include -#include "behaviortree_cpp/python_types.h" +#include "behaviortree_cpp/python/types.h" #endif #ifdef _MSC_VER diff --git a/src/python_bindings.cpp b/src/python/bindings.cpp similarity index 100% rename from src/python_bindings.cpp rename to src/python/bindings.cpp diff --git a/src/python_types.cpp b/src/python/types.cpp similarity index 91% rename from src/python_types.cpp rename to src/python/types.cpp index e41d57f80..717aa6aca 100644 --- a/src/python_types.cpp +++ b/src/python/types.cpp @@ -1,4 +1,4 @@ -#include "behaviortree_cpp/python_types.h" +#include "behaviortree_cpp/python/types.h" #include #include From 84ae12dce804d481d6a00a41c669e4e40f7e7c82 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 3 Sep 2023 19:04:27 -0600 Subject: [PATCH 31/43] Add some type hints to the Python code --- btpy/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/btpy/__init__.py b/btpy/__init__.py index 514f1c0b9..03aa2f3d7 100644 --- a/btpy/__init__.py +++ b/btpy/__init__.py @@ -14,7 +14,7 @@ ) -def ports(inputs=[], outputs=[]): +def ports(inputs: list[str] = [], outputs: list[str] = []): """Decorator to specify input and outputs ports for an action node.""" def specify_ports(cls): @@ -41,11 +41,11 @@ class AsyncActionNode(StatefulActionNode): def __init__(self, name, config): super().__init__(name, config) - def on_start(self): + def on_start(self) -> NodeStatus: self.coroutine = self.run() return NodeStatus.RUNNING - def on_running(self): + def on_running(self) -> NodeStatus: # The library logic should never allow this to happen, but users can # still manually call `on_running` without an associated `on_start` # call. Make sure to print a useful error when this happens. From 4ad738c18c79603e30530a135423578b8f7acf55 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Sun, 3 Sep 2023 19:16:29 -0600 Subject: [PATCH 32/43] Add some docs to Python ex06. --- python_examples/ex06_async_nodes.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/python_examples/ex06_async_nodes.py b/python_examples/ex06_async_nodes.py index 35623295f..064114844 100644 --- a/python_examples/ex06_async_nodes.py +++ b/python_examples/ex06_async_nodes.py @@ -33,12 +33,20 @@ @ports(inputs=["start", "goal"], outputs=["command"]) class MyAsyncNode(AsyncActionNode): + def __init__(self, name, config): + super().__init__(name, config) + self.halted = False + def run(self): start = np.asarray(self.get_input("start")) goal = np.asarray(self.get_input("goal")) + # Here we write an imperative-looking loop, but we place a `yield` call + # at each iteration. This causes the coroutine to yield back to the + # caller until the next iteration of the tree, rather than block the + # main thread. t0 = time.time() - while (t := time.time() - t0) < 1.0: + while (t := time.time() - t0) < 1.0 and not self.halted: command = (1.0 - t) * start + t * goal self.set_output("command", command) yield @@ -47,7 +55,8 @@ def run(self): return NodeStatus.SUCCESS def on_halted(self): - print("Aborted") + # This will be picked up in the main iteration above. + self.halted = True @ports(inputs=["value"]) From 07864754611f4535e74c7d7be93940d74e10d038 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Mon, 4 Sep 2023 18:54:11 -0600 Subject: [PATCH 33/43] Don't make Py_StatefulActionNode final. --- src/python/bindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/bindings.cpp b/src/python/bindings.cpp index b40053226..e30735284 100644 --- a/src/python/bindings.cpp +++ b/src/python/bindings.cpp @@ -31,7 +31,7 @@ class Py_SyncActionNode : public SyncActionNode } }; -class Py_StatefulActionNode final : public StatefulActionNode +class Py_StatefulActionNode : public StatefulActionNode { public: Py_StatefulActionNode(const std::string& name, const NodeConfig& config) : From 0ee0a20bc100304325f7a7e837a31fa6759f2600 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Mon, 4 Sep 2023 18:55:57 -0600 Subject: [PATCH 34/43] Fix some string-embedded XML indentation. --- python_examples/ex01_sample.py | 12 ++++++------ python_examples/ex02_generic_data.py | 12 ++++++------ python_examples/ex04_ros_interop.py | 8 ++++---- python_examples/ex06_async_nodes.py | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/python_examples/ex01_sample.py b/python_examples/ex01_sample.py index 2eb0eda36..e1967bc4f 100644 --- a/python_examples/ex01_sample.py +++ b/python_examples/ex01_sample.py @@ -11,12 +11,12 @@ - - - - - - + + + + + + diff --git a/python_examples/ex02_generic_data.py b/python_examples/ex02_generic_data.py index e7d8c516b..8e6e3f436 100644 --- a/python_examples/ex02_generic_data.py +++ b/python_examples/ex02_generic_data.py @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/python_examples/ex04_ros_interop.py b/python_examples/ex04_ros_interop.py index 78178b2dd..966f60d3f 100644 --- a/python_examples/ex04_ros_interop.py +++ b/python_examples/ex04_ros_interop.py @@ -28,10 +28,10 @@ - - - - + + + + diff --git a/python_examples/ex06_async_nodes.py b/python_examples/ex06_async_nodes.py index 064114844..f8523b297 100644 --- a/python_examples/ex06_async_nodes.py +++ b/python_examples/ex06_async_nodes.py @@ -21,10 +21,10 @@ - - - - + + + + From 93b58c35318a00514f4a0cf4c0d3174684834a24 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Mon, 4 Sep 2023 18:56:08 -0600 Subject: [PATCH 35/43] Formatting. --- src/python/bindings.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/bindings.cpp b/src/python/bindings.cpp index e30735284..71a36e5f3 100644 --- a/src/python/bindings.cpp +++ b/src/python/bindings.cpp @@ -114,7 +114,8 @@ PortsList extractPortsList(const py::type& type) NodeBuilder makeTreeNodeBuilderFn(const py::type& type, const py::args& args, const py::kwargs& kwargs) { - return [=](const auto& name, const auto& config) -> auto { + return [=](const auto& name, const auto& config) -> auto + { py::object obj; obj = type(name, config, *args, **kwargs); From 535ea8801cfd8de54d4299ae98c9a5eaa09493e0 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Mon, 4 Sep 2023 18:56:14 -0600 Subject: [PATCH 36/43] Improve python example README --- python_examples/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python_examples/README.md b/python_examples/README.md index 8e9ec0f31..0c82171eb 100644 --- a/python_examples/README.md +++ b/python_examples/README.md @@ -1,2 +1,3 @@ -1. Install the bindings by running `pip install .` from the project root. -2. Run an example, e.g. `python3 ex01_sample.py` +1. Create a Python virtualenv in the root directory: `python3 -m venv venv && source venv/bin/activate` +2. Build and install the BehaviorTree Python package: `pip install -v .` +3. Run an example, e.g. `python3 python_examples/ex01_sample.py` From 04f435d7c1df1f30d30084ea32319fbadcff5075 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Mon, 4 Sep 2023 19:03:07 -0600 Subject: [PATCH 37/43] Add `halt_tree` binding and use in demo --- python_examples/ex06_async_nodes.py | 15 +++++++-------- src/python/bindings.cpp | 3 ++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/python_examples/ex06_async_nodes.py b/python_examples/ex06_async_nodes.py index f8523b297..cccebcd97 100644 --- a/python_examples/ex06_async_nodes.py +++ b/python_examples/ex06_async_nodes.py @@ -33,10 +33,6 @@ @ports(inputs=["start", "goal"], outputs=["command"]) class MyAsyncNode(AsyncActionNode): - def __init__(self, name, config): - super().__init__(name, config) - self.halted = False - def run(self): start = np.asarray(self.get_input("start")) goal = np.asarray(self.get_input("goal")) @@ -46,7 +42,7 @@ def run(self): # caller until the next iteration of the tree, rather than block the # main thread. t0 = time.time() - while (t := time.time() - t0) < 1.0 and not self.halted: + while (t := time.time() - t0) < 1.0: command = (1.0 - t) * start + t * goal self.set_output("command", command) yield @@ -55,8 +51,7 @@ def run(self): return NodeStatus.SUCCESS def on_halted(self): - # This will be picked up in the main iteration above. - self.halted = True + print("Trajectory halted!") @ports(inputs=["value"]) @@ -73,4 +68,8 @@ def tick(self): factory.register(Print) tree = factory.create_tree_from_text(xml_text) -tree.tick_while_running() + +# Run for a bit, then halt early. +for i in range(0, 10): + tree.tick_once() +tree.halt_tree() diff --git a/src/python/bindings.cpp b/src/python/bindings.cpp index 71a36e5f3..3f2d118ab 100644 --- a/src/python/bindings.cpp +++ b/src/python/bindings.cpp @@ -169,7 +169,8 @@ PYBIND11_MODULE(btpy_cpp, m) .def("tick_once", &Tree::tickOnce) .def("tick_exactly_once", &Tree::tickExactlyOnce) .def("tick_while_running", &Tree::tickWhileRunning, - py::arg("sleep_time") = std::chrono::milliseconds(10)); + py::arg("sleep_time") = std::chrono::milliseconds(10)) + .def("halt_tree", &Tree::haltTree); py::enum_(m, "NodeStatus") .value("IDLE", NodeStatus::IDLE) From 2584aecc0880aadc6c06adf850c0d43ceb1b45b6 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Mon, 4 Sep 2023 19:03:16 -0600 Subject: [PATCH 38/43] Add default impl of AsyncActionNode#on_halted --- btpy/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/btpy/__init__.py b/btpy/__init__.py index 03aa2f3d7..78d8a4580 100644 --- a/btpy/__init__.py +++ b/btpy/__init__.py @@ -32,6 +32,10 @@ class AsyncActionNode(StatefulActionNode): this method can return a final `NodeStatus` value to indicate its exit condition. + Optionally, subclasses can override the `on_halted()` method which is called + when the tree halts. The default implementation does nothing. The `run()` + method will never be called again after a halt. + Note: It is the responsibility of the action author to not block the main behavior tree loop with long-running tasks. `yield` calls should be @@ -64,3 +68,7 @@ def on_running(self) -> NodeStatus: # Otherwise, just assume the action finished successfully. else: return NodeStatus.SUCCESS + + def on_halted(self): + # Default action: do nothing + pass From b425c91e878989712c986a200a7686d1bf6a6ccb Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Mon, 4 Sep 2023 19:17:31 -0600 Subject: [PATCH 39/43] Add docs for `JsonExporter::fromJson`. --- include/behaviortree_cpp/json_export.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/include/behaviortree_cpp/json_export.h b/include/behaviortree_cpp/json_export.h index b6c35644c..d9bf55e42 100644 --- a/include/behaviortree_cpp/json_export.h +++ b/include/behaviortree_cpp/json_export.h @@ -39,10 +39,18 @@ class JsonExporter{ dst = val; } + /** + * @brief fromJson tries to convert arbitrary JSON data into the type T. + * + * Calls only compile if `nlohmann::from_json(const nlohmann::json&, T&)` is + * defined in T's namespace. + */ template T fromJson(const nlohmann::json& src) const { - // TODO: Implement a similar "converter" interface as toJson. + // We don't need to implement a similar `type_converters` interface as + // `toJson` here because the type T must be know statically. There is no + // opaque BT::Any wrapper here requiring RTTI. return src.template get(); } From 1a7ac0a960aab547d86952e39852ff4c86c9e1c7 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Tue, 5 Sep 2023 21:48:23 -0600 Subject: [PATCH 40/43] Properly specify __all__ for btpy module. --- btpy/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/btpy/__init__.py b/btpy/__init__.py index 78d8a4580..e664741f1 100644 --- a/btpy/__init__.py +++ b/btpy/__init__.py @@ -72,3 +72,18 @@ def on_running(self) -> NodeStatus: def on_halted(self): # Default action: do nothing pass + + +# Specify the symbols to be imported with `from btpy import *`, as described in +# [1]. +# +# [1]: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package +__all__ = [ + "ports", + "AsyncActionNode", + "BehaviorTreeFactory", + "NodeStatus", + "StatefulActionNode", + "SyncActionNode", + "Tree", +] From 632eb66dcad8312f95f45ba94904cfaddb5a7c3a Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Tue, 5 Sep 2023 21:55:52 -0600 Subject: [PATCH 41/43] Clean up dummy node use in ex05. --- python_examples/ex05_type_interop.py | 10 +++++----- sample_nodes/dummy_nodes.h | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/python_examples/ex05_type_interop.py b/python_examples/ex05_type_interop.py index a322bce17..66669bfeb 100644 --- a/python_examples/ex05_type_interop.py +++ b/python_examples/ex05_type_interop.py @@ -13,16 +13,16 @@ - + - + - + - + - + diff --git a/sample_nodes/dummy_nodes.h b/sample_nodes/dummy_nodes.h index 0aaa951ed..b57fa8c8e 100644 --- a/sample_nodes/dummy_nodes.h +++ b/sample_nodes/dummy_nodes.h @@ -154,10 +154,10 @@ class RandomVector : public BT::SyncActionNode } }; -class PrintComplex : public BT::SyncActionNode +class PrintMapOfVectors : public BT::SyncActionNode { public: - PrintComplex(const std::string& name, const BT::NodeConfig& config) : + PrintMapOfVectors(const std::string& name, const BT::NodeConfig& config) : BT::SyncActionNode(name, config) {} @@ -167,7 +167,7 @@ class PrintComplex : public BT::SyncActionNode auto input = getInput>("input"); if (input.has_value()) { - std::cerr << "C++: {"; + std::cerr << "{"; for (const auto& [key, value] : *input) { std::cerr << key << ": (" @@ -200,7 +200,7 @@ inline void RegisterNodes(BT::BehaviorTreeFactory& factory) factory.registerNodeType("ApproachObject"); factory.registerNodeType("SaySomething"); factory.registerNodeType("RandomVector"); - factory.registerNodeType("PrintComplex"); + factory.registerNodeType("PrintMapOfVectors"); BT::JsonExporter::get().addConverter(); } From fb33788047a62f790e587e87df64369d560e63ef Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Tue, 5 Sep 2023 21:57:19 -0600 Subject: [PATCH 42/43] Add useful note for ex05 on shared lib location. --- python_examples/ex05_type_interop.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python_examples/ex05_type_interop.py b/python_examples/ex05_type_interop.py index 66669bfeb..c10bb8b2c 100644 --- a/python_examples/ex05_type_interop.py +++ b/python_examples/ex05_type_interop.py @@ -2,6 +2,11 @@ """ Demo of seamless conversion between C++ and Python types. + +NOTE: To run this example, make sure that the path +`sample_nodes/bin/libdummy_nodes_dyn.so` is accessible from the current working +directory. After building the project, this path will exist in your CMake build +root. """ from btpy import BehaviorTreeFactory, SyncActionNode, NodeStatus, ports @@ -34,7 +39,7 @@ class PutVector(SyncActionNode): def tick(self): # Schema matching std::unordered_map - # (defined in dummy_nodes.h, input type of PrintComplex) + # (defined in dummy_nodes.h, input type of PrintMapOfVectors) self.set_output( "output", { From 890396c7d962d65f554e7c0ac356aea66484d3e1 Mon Sep 17 00:00:00 2001 From: Kyle Cesare Date: Tue, 5 Sep 2023 23:45:06 -0600 Subject: [PATCH 43/43] Fix setup.py package attributes. --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 1092583a1..9e97b01bd 100644 --- a/setup.py +++ b/setup.py @@ -127,11 +127,11 @@ def build_extension(self, ext: CMakeExtension) -> None: # The information here can also be placed in setup.cfg - better separation of # logic and declaration, and simpler if you include description/version in a file. setup( - name="btcpp", + name="btpy", version="0.0.1", - author="Kyle Cesare", - author_email="kcesare@gmail.com", - description="Python bindings to the BehaviorTree.CPP project", + author="Davide Faconti", + author_email="davide.faconti@gmail.com", + description="Python bindings to the BehaviorTree.CPP library", long_description="", packages=["btpy"], ext_modules=[CMakeExtension("btcpp")],