When the BT_engine is used, there is usually no need to create new clients. In case a custom client is required or the engine cannot be used, the instantiation of a client can be done as follows.
TickClient client;
client.configure_TickClient("/TickClient", "Test 1");
To use a TickClient we simply have to create one, configure it by giving a <Port Prefix Name>
like "/TickClient"
and a <client name>
like "Test 1"
. The TickClient will open a port with name <Port Prefix Name> + / + <client name> + /tick:o
NOTE: names can contain spaces, they will be replaced with '_' for the port name.
Then we connect the client to the TickServer
client.connect("/TickServer/Test_1"));
After the connection is established, we can ask the TickServer to start its job by ticking it.
The tick request has a mandatory parameters, which is the ActionID and an optional parameter Property <params>
.
client.request_tick(ActionID action, yarp::os::Property params); // parameters are optional
The ActionID is a small structure useful to uniquely identify the client requesting the action, so that the server can safely handle multiple clients at the same time. By using the provided BT_engine, this field is automatically filled by the engine with data coming from the BT description file in XML format.
The fields are:
std::string target;
std::string resources;
std::int32_t action_ID;
target:
this identifies the main target the action is referred to. For example for a navigation action, the target is the location to reach.
resources:
this identifies which resources the action will need to use, for example for a grasping action this may indicate to perform the movement with left or right arm.
action_ID:
an ID number which is required to be unique for each Behaviour Tree for each node. In case the same action node is repeated more than once, each instance needs to have a different ID.
For example the two leaf nodes "Go to Kitchen" and action "Go To Living Room" may be implemented as a single client
in which the target is different; in this case also the action_ID
number has to be different.
The yarp::os::Property can contain any addictional parameter required by the server to perform the action. The Property is a dictionary type container able to store any data type.
To implement a server, a dedicated class has to be created by inheriting from TickServer:
class MySkillClass : public TickServer
and then the methods request_tick
, request_halt
have to be implemented.
The return value is the enum ReturnStatus
; the halt function shall return either BT_HALTED
or BT_ERROR
in case an error occurs.
The routine request_tick
should not take time to execute and return immediately a value. In case more time is required,
then it has to be executed in a separated thread. In order to do so, when configuring the TickServer simply set the
flag to true, like in the following example.
skill.configure_TickServer("/TickServer", "Test 1", true);
NOTE: The request_tick
function will be executed only when it is necessary and for each target independently.
This means:
- If a
request_tick
is already running for a target, no other calls torequest_tick
will be executed for the same target, until the previous execution terminates. - Different
request_tick
calls for different targets can run in parallel. All the relevant values like status, parameters etc are handled independently so the implementation does not have to to worry about.
In case your server requires to store some custom data separately for each target, the TickServer_withData<T>
can be used instead.
This class is an extension of TickServer with an addictional member std::map userData<ActionID, T>
In this way, in the request_tick
implementation, the map can be used to store and retrieve custom data by using the ActionID
.
For example, in case it is required to store an integer counter:
class MySkillClass : public TickServer_withData<int>
{
... // no need to declare the int, we will use the userData map already defined.
ReturnStatus request_tick(const yarp::BT_wrappers::ActionID &target, const yarp::os::Property ¶ms) override
{
// in case this is the first time this target is requested, initialize the data
if (userData.find(target) == userData.end())
userData[target] = 0;
userData[target]++; // increment the counter
// print it
yInfo() << " counter now at " << userData[target] << " for target " << target.target;
}
...
}
NOTE: The function request_halt
is blocking and waiting for the corresponding request_tick
thread (if any) to terminate the execution before returning. This is to ensure that when the request_halt
returns, the module is really halted;
the drawback is that, in case the request_tick
or request_halt
takes time to terminate, the execution of the behaviour tree is freezed until they exit.
It is important therefore to verify if a halt has been requested and terminate as soon as possible. For example:
request_tick(...)
{
...
while(work_to_do && !isHaltRequested(target))
{
// do my work here
}
...
}
The function isHaltRequested(target)
will return true if and only if a halt request has been received for the specified target. If a halt has been requested for a different target, it will return false and the execution will proceed.
An importante piece of the infrastructure is the shared YARP BlackBoard. It is a process working as a memory shared between all the other process running and the Behavior Tree engine. When nodes needs to share information between them or a node implementation requires more parameters to correctly perform its task, these information can be stored in and retrieved from the BlackBoard.
The memory is accessible as a dictionary <key, value>
where the key is a std::string
and the value is a yarp::os::Property
. The Property
is YARP-based dictionary-like container, in such a way the maximum flexibility can be achieved in storing custom data.
In conjunction with the YARP BT wrappers, the key
of the YARP BlackBoard is meant to be the Action_ID target
described before.
This allows a TickClient/Server to get the parameters from the BlackBoard or to write updated information using information already available and well known to the user.
In order to communicate with the BlackBoard, a BlackBoardClient
has to instantiate and configured, like shown:
m_blackboardClient.configureBlackBoardClient("", getName());
m_blackboardClient.connectToBlackBoard(remoteBB_name);
Then data can be get/set by using the corresponding functions:
yarp::os::Property datum;
datum.put("Grasped", yarp::os::Value(true));
m_blackboardClient.setData("myTarget", datum);
Property values = m_blackboardClient.getData("myTarget");
NOTE: When setting data to the BlackBoard, the new information will be merged with existing ones, adding a new field if not present. This means there is no need to get the data, change it and set it back.
Imagine there are two nodes that manipulates data relative to the same target, like FindObject
and GraspObject
modules
yarp::os::Property datum;
datum.put("Found", yarp::os::Value(true));
m_blackboardClient.setData("myCup", datum);
The BlackBoard will now have an entry target
myCup with the field Found
set to true
.
yarp::os::Property datum;
datum.put("Grasped", yarp::os::Value(true));
m_blackboardClient.setData("myCup", datum);
After this new setData
the target
myCup will also contain the addictional field Grasped
set to true
.