Custom nodes¶
When the built-in nodes are not enough, write your own. A custom node is a small piece of code that runs when the tree reaches it: it reads its input ports, does its work, writes its output ports and answers SUCCESS, FAILURE or RUNNING.
There are three ways to give a robot your own nodes:
Way |
When to use it |
|---|---|
Code nodes (Behaviour page → Code nodes) |
The usual way. Write the node in Python, C++, JavaScript or Bash on the dashboard; it is sent to every robot of the project and loaded straight away. |
External plugin paths |
The code is already installed on the robot (your own ROS 2 package, for example). XPARO only gets the path. |
Custom Nodes (inline code) |
Python typed into the editor’s Custom Nodes panel. Off by default per project; changes are audit-logged. |
The rest of this page covers code nodes; the other two use the same Python class shape.
How a code node gets to the robot¶
You create the file and expose it as a node on the Behaviour page (XML tag, Action or Condition, ports).
When you save, the server sends the source and the node’s settings to every connected robot (and to each robot when it next connects).
The robot writes the file to
custom_behaviors/custom_node_files/<language>/<name>.<py|cpp|js|sh>, compiles it if it is C++, and registers its tag.It answers with the list of tags it registered and any failures; the dashboard shows a failure as a red message with the reason (for example a C++ compile error).
The robot’s log confirms what it loaded:
[bt_engine] loaded custom node file tags: ['RoomIsFree']
Ports¶
Ports are the node’s inputs and outputs, written as XML attributes where the node is used in a tree:
<GreetPython person="{who}" greeting="msg" />
An input is a fixed value (
person="Asha") or a blackboard value (person="{who}").An output names the blackboard entry to write, without braces:
greeting="msg"writes the result tomsg, which later nodes read as{msg}. (Writinggreeting="{msg}"would create an entry literally called{msg}.)
The examples below are the dashboard’s own generated templates for a node
with one input person and one output greeting, with the one line of
logic filled in. Each was run on the robot engine and wrote
msg = "Hello Asha from …".
Python¶
from xparo.bt_engine.plugin_loader import CustomBTNode
from xparo.bt_engine.nodes.base import resolve_attrs, write_output
from py_trees.common import Status
class GreetPython(CustomBTNode):
XML_TAG = "GreetPython"
def update(self):
attrs = resolve_attrs(self.attrs, self.blackboard, required=("person",))
person = attrs.get("person", None)
write_output(self.attrs, self.blackboard, "greeting", f"Hello {person} from Python")
return Status.SUCCESS
XML_TAGis the tag trees use. It must match the tag set on the dashboard.update()runs on every tick (10 times a second) while the node is active. ReturnStatus.RUNNINGfor work that takes longer, and never block insideupdate(): the whole tree waits for it.resolve_attrsturns the XML attributes into values ({name}read from the blackboard);requirednames inputs that must have a value.write_outputwrites an output port.self.feedback_messageis shown in the run report, and as the reason when the node fails:The tree finished with FAILURE at Room_is_free (RoomIsFree): icu is busy.self.ros_nodeis the robot’sxparo_rosnode, for publishing to topics or calling services (Nonewhen the engine runs without ROS 2).To clean up when the node is stopped early (the task was cancelled, hit its time limit, or its parent moved on), override
terminate(self, new_status); it is called withStatus.INVALID. The generated template has ahalt()method, but the engine never calls it for Python nodes.If
update()raises an exception, that node fails withcrashed: …in its message; the rest of the tree carries on as for any failure.
This is the condition used in the docs’ demo project, which fails for busy rooms:
from xparo.bt_engine.plugin_loader import CustomBTNode
from xparo.bt_engine.nodes.base import resolve_attrs
from py_trees.common import Status
class RoomIsFree(CustomBTNode):
XML_TAG = "RoomIsFree"
def update(self):
attrs = resolve_attrs(self.attrs, self.blackboard, required=("room",))
busy_rooms = {"icu", "operating_theatre"}
if attrs["room"] in busy_rooms:
self.feedback_message = f"{attrs['room']} is busy"
return Status.FAILURE
return Status.SUCCESS
C++¶
#include <behaviortree_cpp/behavior_tree.h>
class GreetCpp : public BT::SyncActionNode
{
public:
GreetCpp(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config) {}
static BT::PortsList providedPorts()
{
return {
BT::InputPort<std::string>("person"),
BT::OutputPort<std::string>("greeting")
};
}
BT::NodeStatus tick() override
{
auto person = getInput<std::string>("person");
setOutput("greeting", std::string("Hello ") + person.value() + " from C++");
return BT::NodeStatus::SUCCESS;
}
};
A normal BehaviorTree.CPP node: derive from
BT::SyncActionNode(action) orBT::ConditionNode(condition). Both finish within one tick, so returnSUCCESSorFAILURE(notRUNNING).The robot compiles it with
g++ -std=c++17against the BehaviorTree.CPP of its ROS 2 installation, so it needsg++,ros-jazzy-behaviortree-cppandnlohmann-json3-dev. Extra libraries cannot be linked.Each node in a tree runs as its own small program that stays alive for the run; every tick must answer within 5 seconds.
JavaScript¶
class GreetJavascript extends XparoNode {
tick() {
const person = this.input("person", null);
this.output("greeting", `Hello ${person} from JavaScript`);
return this.SUCCESS; // or this.RUNNING / this.FAILURE
}
halt() {
// Only needed if tick() can return this.RUNNING.
}
}
module.exports = GreetJavascript;
Needs Node.js (the
nodecommand) on the robot.XparoNodeis provided by XPARO; don’trequireit.One Node.js process per node in a tree, alive for the whole run, so your object keeps its state between ticks.
halt()is called when it is stopped.this.input(key, default)reads an input;this.output(key, value)writes an output.Log with
console.erroronly: standard output carries XPARO’s messages. Every tick must answer within 5 seconds.
Bash¶
#!/usr/bin/env bash
set -euo pipefail
PERSON="${PERSON:-}"
echo "GREETING=Hello ${PERSON} from Bash"
exit 0 # SUCCESS, or 1 for FAILURE, or 2 for RUNNING
Inputs arrive as environment variables named in upper case (
person→$PERSON).Outputs are
KEY=valuelines on standard output (GREETING=…→ thegreetingport).The exit code is the result:
0SUCCESS,1FAILURE,2RUNNING.The script runs again from scratch on every tick and must finish within 5 seconds. XPARO makes the file executable.
External plugin paths¶
If your node code is already on the robot, add its path under External
Plugin Paths on the Behaviour page and switch it on. A path can be a Python
file (/opt/my_nodes/nodes.py) or an installed module
(my_package.bt_nodes). Every CustomBTNode subclass with an
XML_TAG inside it becomes usable in trees. Only the path travels over the
network; the code itself must be on each robot.
Inline Custom Nodes¶
The Custom Nodes panel on the Behaviour page stores Python node classes in the project itself and sends them to robots. Because that code runs inside the robot’s XPARO process, it is switched off for new projects: an Owner or Editor must allow it for the project first, and every created, changed or deleted node is recorded in an audit log.
When a node isn’t found¶
A tree using a tag the robot doesn’t know is refused before it runs, with the
outcome invalid_tree and <Tag> isn't a node this robot knows. Check:
the robot was online when you saved the node (or has reconnected since);
the dashboard showed no red sync error for the file;
for Python,
XML_TAGequals the tag used in the tree;for an external plugin path, the path is switched on and the file exists on that robot.