Behaviour Tree for memory constrained devices. icpmoles.tngl.io/orchard
README.md

C++ example #

The example tree driving a simulated board from C++14, through orchard/orchard.hpp: member functions as leaves, a Tree<64> that owns its pool, and the example tree either loaded from its JSON file or compiled into the program.

Build and run #

From the repository root, with a C++ compiler installed:

cmake -B build && cmake --build build
./build/orchard_cpp_demo examples/posix/tree.json   # the tree from its file
./build/orchard_cpp_demo --code                     # the same tree, built by the compiler
ctest --test-dir build

CMake builds this example only when it finds a C++ compiler, so a C-only toolchain still builds the library and the C examples. ctest runs it both ways as tests. Both print the same timeline.

The program does not read the keyboard. It plays a fixed timeline of button presses on a simulated clock, so a run takes no time and always prints the same output:

  1200 ms  button 0 pressed
  1200 ms  leds **...
  1300 ms  leds *....
  ...
  2200 ms  button 1 pressed
  2200 ms  leds *.**.
  2600 ms  button 2 pressed
  2600 ms  leds *....

Each branch of the tree gets its turn: button 0 blinks LED 1, button 1 turns on LEDs 2 and 3 until button 2 blocks it, and LED 0 blinks as a heartbeat the rest of the time. At the end it prints the tree with the status of the last tick.

How it works #

main.cpp is about 180 lines.

Leaves are member functions. Board holds the simulated buttons and LEDs, and its methods button(), led_on(), led_off() and led_toggle() are the leaves. orchard::member_leaf turns each into the plain function orchard calls, and finds the Board through the tree's user pointer:

constexpr orchard_leaf_def leaves[] = {
	{"button", orchard::member_leaf<Board, &Board::button>},
	{"led_on", orchard::member_leaf<Board, &Board::led_on>},
	...
};

Board board;
orchard::Tree<64> tree(leaves, &board, sim_clock);

Exceptions stop at the leaf. The leaves use std::array::at(), which throws for an index out of range: a tree file with "arg": 9 for an LED throws in that leaf. member_leaf catches it, so it never travels through orchard's C code, and the leaf fails while the rest of the tree goes on. The example prints the message through orchard::set_exception_handler().

The tree, compiled. With --code, the tree is not read from a file: it is written in C++, nested like the JSON, and orchard::compile() turns it into a plan at compile time. A misspelled leaf name or a negative wait would be a build error, and tree.load() cannot fail:

constexpr auto example_tree = compile(leaves,
	reactive_fallback(
		reactive_sequence(condition("button", 0),
				  sequence(action("led_on", 1), wait(100), action("led_off", 1), wait(100))),
		...));

A custom clock. The tree takes any orchard_time_t (*)(void) as the clock for wait nodes. The example passes a function that reads the simulated time, so a 500 ms wait takes 50 iterations of the loop and no real time. On a real target, leave the default, orchard_port_now_ms.

No heap. The pool is inside the Tree, and the Tree, the Board and the file buffer are globals. Tree's constructor is constexpr, so they are initialized by the compiler, with no code at startup.

See Using from C++ for the whole interface.