Coverage Report

Created: 2026-05-30 06:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/behaviortreecpp/src/tree_node.cpp
Line
Count
Source
1
/* Copyright (C) 2015-2018 Michele Colledanchise -  All Rights Reserved
2
 * Copyright (C) 2018-2025 Davide Faconti, Eurecat -  All Rights Reserved
3
*
4
*   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
5
*   to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
6
*   and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
*   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
*
9
*   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
10
*   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
11
*   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12
*/
13
14
#include "behaviortree_cpp/tree_node.h"
15
16
#include <array>
17
#include <atomic>
18
#include <cstring>
19
#include <vector>
20
21
namespace BT
22
{
23
24
struct TreeNode::PImpl
25
{
26
  PImpl(std::string name, NodeConfig config)
27
5.95k
    : name(std::move(name)), config(std::move(config))
28
5.95k
  {}
29
30
  const std::string name;
31
32
  NodeStatus status = NodeStatus::IDLE;
33
34
  std::condition_variable state_condition_variable;
35
36
  mutable std::mutex state_mutex;
37
38
  StatusChangeSignal state_change_signal;
39
40
  NodeConfig config;
41
42
  std::string registration_ID;
43
44
  PreTickCallback pre_tick_callback;
45
  PostTickCallback post_tick_callback;
46
  TickMonitorCallback tick_monitor_callback;
47
48
  std::mutex callback_injection_mutex;
49
50
  std::shared_ptr<WakeUpSignal> wake_up;
51
52
  std::array<ScriptFunction, size_t(PreCond::COUNT_)> pre_parsed;
53
  std::array<ScriptFunction, size_t(PostCond::COUNT_)> post_parsed;
54
};
55
56
TreeNode::TreeNode(std::string name, NodeConfig config)
57
5.95k
  : _p(new PImpl(std::move(name), std::move(config)))
58
5.95k
{}
59
60
0
TreeNode::TreeNode(TreeNode&& other) noexcept : _p(std::move(other._p))
61
0
{}
62
63
TreeNode& TreeNode::operator=(TreeNode&& other) noexcept
64
0
{
65
0
  this->_p = std::move(other._p);
66
0
  return *this;
67
0
}
68
69
5.95k
TreeNode::~TreeNode() = default;
70
71
NodeStatus TreeNode::executeTick()
72
0
{
73
0
  auto new_status = _p->status;
74
0
  PreTickCallback pre_tick;
75
0
  PostTickCallback post_tick;
76
0
  TickMonitorCallback monitor_tick;
77
0
  {
78
0
    const std::scoped_lock lk(_p->callback_injection_mutex);
79
0
    pre_tick = _p->pre_tick_callback;
80
0
    post_tick = _p->post_tick_callback;
81
0
    monitor_tick = _p->tick_monitor_callback;
82
0
  }
83
84
  // a pre-condition may return the new status.
85
  // In this case it override the actual tick()
86
0
  if(auto precond = checkPreConditions())
87
0
  {
88
0
    new_status = precond.value();
89
0
  }
90
0
  else
91
0
  {
92
    // injected pre-callback
93
0
    bool substituted = false;
94
0
    if(pre_tick && !isStatusCompleted(_p->status))
95
0
    {
96
0
      auto override_status = pre_tick(*this);
97
0
      if(isStatusCompleted(override_status))
98
0
      {
99
        // don't execute the actual tick()
100
0
        substituted = true;
101
0
        new_status = override_status;
102
0
      }
103
0
    }
104
105
    // Call the ACTUAL tick
106
0
    if(!substituted)
107
0
    {
108
0
      using namespace std::chrono;
109
      // Use atomic_thread_fence to prevent compiler reordering of time measurements.
110
      // See issue #861 for details.
111
0
      const auto t1 = steady_clock::now();
112
0
      std::atomic_thread_fence(std::memory_order_seq_cst);
113
0
      try
114
0
      {
115
0
        new_status = tick();
116
0
      }
117
0
      catch(const NodeExecutionError&)
118
0
      {
119
        // Already wrapped by a child node, re-throw as-is to preserve original info
120
0
        throw;
121
0
      }
122
0
      catch(const std::exception& ex)
123
0
      {
124
        // Wrap the exception with this node's context
125
0
        throw NodeExecutionError({ name(), fullPath(), registrationName() }, ex.what());
126
0
      }
127
0
      std::atomic_thread_fence(std::memory_order_seq_cst);
128
0
      const auto t2 = steady_clock::now();
129
0
      if(monitor_tick)
130
0
      {
131
0
        monitor_tick(*this, new_status, duration_cast<microseconds>(t2 - t1));
132
0
      }
133
0
    }
134
0
  }
135
136
  // injected post callback
137
0
  if(isStatusCompleted(new_status))
138
0
  {
139
0
    checkPostConditions(new_status);
140
0
  }
141
142
0
  if(post_tick)
143
0
  {
144
0
    auto override_status = post_tick(*this, new_status);
145
0
    if(isStatusCompleted(override_status))
146
0
    {
147
0
      new_status = override_status;
148
0
    }
149
0
  }
150
151
  // preserve the IDLE state if skipped, but communicate SKIPPED to parent
152
0
  if(new_status != NodeStatus::SKIPPED)
153
0
  {
154
0
    setStatus(new_status);
155
0
  }
156
0
  return new_status;
157
0
}
158
159
void TreeNode::haltNode()
160
6.54k
{
161
6.54k
  halt();
162
163
6.54k
  const auto& parse_executor = _p->post_parsed[size_t(PostCond::ON_HALTED)];
164
6.54k
  if(parse_executor)
165
0
  {
166
0
    Ast::Environment env = { config().blackboard, config().enums };
167
0
    parse_executor(env);
168
0
  }
169
6.54k
}
170
171
void TreeNode::setStatus(NodeStatus new_status)
172
0
{
173
0
  if(new_status == NodeStatus::IDLE)
174
0
  {
175
0
    throw RuntimeError("Node [", name(),
176
0
                       "]: you are not allowed to set manually the status to IDLE. "
177
0
                       "If you know what you are doing (?) use resetStatus() instead.");
178
0
  }
179
180
0
  NodeStatus prev_status = NodeStatus::IDLE;
181
0
  {
182
0
    const std::unique_lock<std::mutex> UniqueLock(_p->state_mutex);
183
0
    prev_status = _p->status;
184
0
    _p->status = new_status;
185
0
  }
186
0
  if(prev_status != new_status)
187
0
  {
188
0
    _p->state_condition_variable.notify_all();
189
0
    _p->state_change_signal.notify(std::chrono::high_resolution_clock::now(), *this,
190
0
                                   prev_status, new_status);
191
0
  }
192
0
}
193
194
TreeNode::PreScripts& TreeNode::preConditionsScripts()
195
5.95k
{
196
5.95k
  return _p->pre_parsed;
197
5.95k
}
198
199
TreeNode::PostScripts& TreeNode::postConditionsScripts()
200
5.91k
{
201
5.91k
  return _p->post_parsed;
202
5.91k
}
203
204
Expected<NodeStatus> TreeNode::checkPreConditions()
205
0
{
206
0
  Ast::Environment env = { config().blackboard, config().enums };
207
208
  // Check pre-conditions in order: FAILURE_IF, SUCCESS_IF, SKIP_IF, WHILE_TRUE.
209
  // IMPORTANT: _failureIf, _successIf, and _skipIf are evaluated ONLY when the
210
  // node is IDLE or SKIPPED. They are NOT re-evaluated while the node is RUNNING.
211
0
  for(size_t index = 0; index < size_t(PreCond::COUNT_); index++)
212
0
  {
213
0
    const auto& parse_executor = _p->pre_parsed[index];
214
0
    if(!parse_executor)
215
0
    {
216
0
      continue;
217
0
    }
218
219
0
    const auto preID = static_cast<PreCond>(index);
220
221
    // _failureIf, _successIf, _skipIf: only checked when IDLE or SKIPPED
222
    // _while: checked here AND also when RUNNING (see below)
223
0
    if(_p->status == NodeStatus::IDLE || _p->status == NodeStatus::SKIPPED)
224
0
    {
225
      // what to do if the condition is true
226
0
      if(parse_executor(env).cast<bool>())
227
0
      {
228
0
        if(preID == PreCond::FAILURE_IF)
229
0
        {
230
0
          return NodeStatus::FAILURE;
231
0
        }
232
0
        if(preID == PreCond::SUCCESS_IF)
233
0
        {
234
0
          return NodeStatus::SUCCESS;
235
0
        }
236
0
        if(preID == PreCond::SKIP_IF)
237
0
        {
238
0
          return NodeStatus::SKIPPED;
239
0
        }
240
0
      }
241
      // if the conditions is false
242
0
      else if(preID == PreCond::WHILE_TRUE)
243
0
      {
244
0
        return NodeStatus::SKIPPED;
245
0
      }
246
0
    }
247
0
    else if(_p->status == NodeStatus::RUNNING && preID == PreCond::WHILE_TRUE)
248
0
    {
249
      // _while is the ONLY precondition checked while RUNNING.
250
      // If the condition becomes false, halt the node and return SKIPPED.
251
0
      if(!parse_executor(env).cast<bool>())
252
0
      {
253
0
        haltNode();
254
0
        return NodeStatus::SKIPPED;
255
0
      }
256
0
    }
257
0
  }
258
0
  return nonstd::make_unexpected("");  // no precondition
259
0
}
260
261
void TreeNode::checkPostConditions(NodeStatus status)
262
0
{
263
0
  auto ExecuteScript = [this](const PostCond& cond) {
264
0
    const auto& parse_executor = _p->post_parsed[size_t(cond)];
265
0
    if(parse_executor)
266
0
    {
267
0
      Ast::Environment env = { config().blackboard, config().enums };
268
0
      parse_executor(env);
269
0
    }
270
0
  };
271
272
0
  if(status == NodeStatus::SUCCESS)
273
0
  {
274
0
    ExecuteScript(PostCond::ON_SUCCESS);
275
0
  }
276
0
  else if(status == NodeStatus::FAILURE)
277
0
  {
278
0
    ExecuteScript(PostCond::ON_FAILURE);
279
0
  }
280
0
  ExecuteScript(PostCond::ALWAYS);
281
0
}
282
283
void TreeNode::resetStatus()
284
12.9k
{
285
12.9k
  NodeStatus prev_status = NodeStatus::IDLE;
286
12.9k
  {
287
12.9k
    const std::unique_lock<std::mutex> lock(_p->state_mutex);
288
12.9k
    prev_status = _p->status;
289
12.9k
    _p->status = NodeStatus::IDLE;
290
12.9k
  }
291
292
12.9k
  if(prev_status != NodeStatus::IDLE)
293
0
  {
294
0
    _p->state_condition_variable.notify_all();
295
0
    _p->state_change_signal.notify(std::chrono::high_resolution_clock::now(), *this,
296
0
                                   prev_status, NodeStatus::IDLE);
297
0
  }
298
12.9k
}
299
300
NodeStatus TreeNode::status() const
301
4.47k
{
302
4.47k
  const std::lock_guard<std::mutex> lock(_p->state_mutex);
303
4.47k
  return _p->status;
304
4.47k
}
305
306
NodeStatus TreeNode::waitValidStatus()
307
0
{
308
0
  std::unique_lock<std::mutex> lock(_p->state_mutex);
309
310
0
  while(isHalted())
311
0
  {
312
0
    _p->state_condition_variable.wait(lock);
313
0
  }
314
0
  return _p->status;
315
0
}
316
317
const std::string& TreeNode::name() const
318
0
{
319
0
  return _p->name;
320
0
}
321
322
bool TreeNode::isHalted() const
323
0
{
324
0
  return _p->status == NodeStatus::IDLE;
325
0
}
326
327
TreeNode::StatusChangeSubscriber
328
TreeNode::subscribeToStatusChange(TreeNode::StatusChangeCallback callback)
329
0
{
330
0
  return _p->state_change_signal.subscribe(std::move(callback));
331
0
}
332
333
void TreeNode::setPreTickFunction(PreTickCallback callback)
334
0
{
335
0
  const std::unique_lock lk(_p->callback_injection_mutex);
336
0
  _p->pre_tick_callback = std::move(callback);
337
0
}
338
339
void TreeNode::setPostTickFunction(PostTickCallback callback)
340
0
{
341
0
  const std::unique_lock lk(_p->callback_injection_mutex);
342
0
  _p->post_tick_callback = std::move(callback);
343
0
}
344
345
void TreeNode::setTickMonitorCallback(TickMonitorCallback callback)
346
0
{
347
0
  const std::unique_lock lk(_p->callback_injection_mutex);
348
0
  _p->tick_monitor_callback = std::move(callback);
349
0
}
350
351
uint16_t TreeNode::UID() const
352
561
{
353
561
  return _p->config.uid;
354
561
}
355
356
const std::string& TreeNode::fullPath() const
357
2
{
358
2
  return _p->config.path;
359
2
}
360
361
const std::string& TreeNode::registrationName() const
362
0
{
363
0
  return _p->registration_ID;
364
0
}
365
366
const NodeConfig& TreeNode::config() const
367
10
{
368
10
  return _p->config;
369
10
}
370
371
NodeConfig& TreeNode::config()
372
15.4k
{
373
15.4k
  return _p->config;
374
15.4k
}
375
376
StringView TreeNode::getRawPortValue(const std::string& key) const
377
0
{
378
0
  auto remap_it = _p->config.input_ports.find(key);
379
0
  if(remap_it == _p->config.input_ports.end())
380
0
  {
381
0
    remap_it = _p->config.output_ports.find(key);
382
0
    if(remap_it == _p->config.output_ports.end())
383
0
    {
384
0
      throw std::logic_error(StrCat("[", key, "] not found"));
385
0
    }
386
0
  }
387
0
  return remap_it->second;
388
0
}
389
390
bool TreeNode::isBlackboardPointer(StringView str, StringView* stripped_pointer)
391
1.55k
{
392
1.55k
  if(str.size() < 3)
393
789
  {
394
789
    return false;
395
789
  }
396
  // strip leading and following spaces
397
770
  size_t front_index = 0;
398
770
  size_t last_index = str.size() - 1;
399
966
  while(str[front_index] == ' ' && front_index <= last_index)
400
196
  {
401
196
    front_index++;
402
196
  }
403
872
  while(str[last_index] == ' ' && front_index <= last_index)
404
102
  {
405
102
    last_index--;
406
102
  }
407
770
  const auto size = (last_index - front_index) + 1;
408
770
  auto valid = size >= 3 && str[front_index] == '{' && str[last_index] == '}';
409
770
  if(valid && stripped_pointer != nullptr)
410
150
  {
411
150
    *stripped_pointer = StringView(&str[front_index + 1], size - 2);
412
150
  }
413
770
  return valid;
414
1.55k
}
415
416
StringView TreeNode::stripBlackboardPointer(StringView str)
417
150
{
418
150
  StringView out;
419
150
  if(isBlackboardPointer(str, &out))
420
150
  {
421
150
    return out;
422
150
  }
423
0
  return {};
424
150
}
425
426
Expected<StringView> TreeNode::getRemappedKey(StringView port_name,
427
                                              StringView remapped_port)
428
0
{
429
0
  if(remapped_port == "{=}" || remapped_port == "=")
430
0
  {
431
0
    return { port_name };
432
0
  }
433
0
  StringView stripped;
434
0
  if(isBlackboardPointer(remapped_port, &stripped))
435
0
  {
436
0
    return { stripped };
437
0
  }
438
0
  return nonstd::make_unexpected("Not a blackboard pointer");
439
0
}
440
441
void TreeNode::emitWakeUpSignal()
442
0
{
443
0
  if(_p->wake_up)
444
0
  {
445
0
    _p->wake_up->emitSignal();
446
0
  }
447
0
}
448
449
bool TreeNode::requiresWakeUp() const
450
0
{
451
0
  return bool(_p->wake_up);
452
0
}
453
454
void TreeNode::setRegistrationID(StringView ID)
455
10.4k
{
456
10.4k
  _p->registration_ID.assign(ID.data(), ID.size());
457
10.4k
}
458
459
void TreeNode::setWakeUpInstance(std::shared_ptr<WakeUpSignal> instance)
460
3.25k
{
461
3.25k
  _p->wake_up = instance;
462
3.25k
}
463
464
void TreeNode::modifyPortsRemapping(const PortsRemapping& new_remapping)
465
0
{
466
0
  for(const auto& new_it : new_remapping)
467
0
  {
468
0
    auto it = _p->config.input_ports.find(new_it.first);
469
0
    if(it != _p->config.input_ports.end())
470
0
    {
471
0
      it->second = new_it.second;
472
0
    }
473
0
    it = _p->config.output_ports.find(new_it.first);
474
0
    if(it != _p->config.output_ports.end())
475
0
    {
476
0
      it->second = new_it.second;
477
0
    }
478
0
  }
479
0
}
480
481
template <>
482
std::string toStr<PreCond>(const PreCond& cond)
483
23.8k
{
484
23.8k
  if(cond < PreCond::COUNT_)
485
23.8k
  {
486
23.8k
    return BT::PreCondNames[static_cast<size_t>(cond)];
487
23.8k
  }
488
0
  return "Undefined";
489
23.8k
}
490
491
template <>
492
std::string toStr<PostCond>(const PostCond& cond)
493
23.8k
{
494
23.8k
  if(cond < BT::PostCond::COUNT_)
495
23.8k
  {
496
23.8k
    return BT::PostCondNames[static_cast<size_t>(cond)];
497
23.8k
  }
498
0
  return "Undefined";
499
23.8k
}
500
501
AnyPtrLocked BT::TreeNode::getLockedPortContent(const std::string& key)
502
0
{
503
0
  if(auto remapped_key = getRemappedKey(key, getRawPortValue(key)))
504
0
  {
505
0
    const auto bb_key = std::string(*remapped_key);
506
0
    auto result = _p->config.blackboard->getAnyLocked(bb_key);
507
0
    if(!result && _p->config.manifest != nullptr)
508
0
    {
509
      // Entry doesn't exist yet. Create it using the port's type info
510
      // from the manifest so that getLockedPortContent works even when
511
      // the port is not explicitly declared in XML. Issue #942.
512
0
      auto port_it = _p->config.manifest->ports.find(key);
513
0
      if(port_it != _p->config.manifest->ports.end())
514
0
      {
515
0
        _p->config.blackboard->createEntry(bb_key, port_it->second);
516
0
        result = _p->config.blackboard->getAnyLocked(bb_key);
517
0
      }
518
0
    }
519
0
    return result;
520
0
  }
521
0
  return {};
522
0
}
523
524
}  // namespace BT