Skip to content

lua: Add Lua language support to allow pre/post conditions#46

Merged
otavio merged 2 commits intomasterfrom
lua/add_initial_support
Apr 19, 2026
Merged

lua: Add Lua language support to allow pre/post conditions#46
otavio merged 2 commits intomasterfrom
lua/add_initial_support

Conversation

@nandojve
Copy link
Copy Markdown
Contributor

@nandojve nandojve commented Apr 17, 2026

  • Add BehaviorTree.CPP-style pre/post conditions (_failureIf, _successIf, _skipIf, _while, _onFailure, _onSuccess, _post) evaluated via Lua on any behaviour tree node; fully optional behind CONFIG_ZEPHYR_BEHAVIOUR_TREE_LUA_CONDITIONS.
  • Add robot_patrol advanced sample demonstrating a ROS-like mobile robot patrol on an 8×8 grid, exercising subtrees with port aliasing, user Lua override files, shared C/Lua blackboard, and BFS pathfinding.
  • Several engine fixes included: port mapping now applies to Lua condition scripts, addBlackboardVariables() processes all node attributes, and per-instance path storage prevents clobbering between NavigateTo subtree instances.

Test Plan

  • Build passes on native_sim/native/64
  • lua_conditions sample pytest passes on native_sim/native/64
  • robot_patrol sample YAML regex patterns verify full lifecycle
    (home → patrol → battery emergency → recharge → mission complete)
  • Confirm CONFIG_ROBOT_PATROL_TEST_MODE enables fast CI execution

Fixes #45

ZephyrBT-Robot-Patrol.mp4

CC: @Z0rdon @rodrigopex

@nandojve nandojve force-pushed the lua/add_initial_support branch from 1fc912c to c0d7fdc Compare April 17, 2026 15:37
@nandojve nandojve requested a review from otavio April 17, 2026 15:37
Add support for BehaviorTree.CPP-style pre/post conditions on behaviour
tree nodes, using Lua (via lua_zephyr module) as the script engine.

Conditions are defined as XML attributes on any node and evaluated
inline during tree evaluation:

  Pre-conditions (checked before tick, in order):
    _failureIf  - return FAILURE if script is true
    _successIf  - return SUCCESS if script is true
    _skipIf     - return SKIP if script is true
    _while      - return SKIP if script is false; re-checked on RUNNING

  Post-conditions (executed after tick):
    _onFailure  - run script when node returns FAILURE
    _onSuccess  - run script when node returns SUCCESS
    _post       - always run after SUCCESS or FAILURE
    _onHalted   - reserved for future halt support

Scripts use pure Lua syntax with blackboard access via a "bb" userdata
passed as argument to each condition function. Pre-condition functions
return a boolean, post-condition functions execute side effects.

Example XML:
  <Increment _skipIf="bb.counter > 3" _post="bb.ticks = bb.ticks + 1"/>

Architecture:
- The code generator extracts _-prefixed condition attributes from XML,
  separating them from normal port attributes.
- A {name}_gen.lua file is generated with weak-guarded named functions:
    if not inc_counter_skip_if then
    function inc_counter_skip_if(bb)
        return bb.counter > 3
    end
    end
  Function naming: {node_name}_{condition_type} using the XML name attr.
- The .lua file is embedded as a C string header via lua_zephyr's
  luaz_gen.py, producing {name}_gen_lua_script.h.
- Users can override any condition by providing .lua files loaded before
  the generated one. Since generated functions use "if not ... then"
  guards, user definitions take precedence (weak/strong pattern).
- The _data.c file contains a zephyrbt_lua_cond_ref[] array mapping
  node indices and condition types to function names, plus bb_names[]
  for blackboard variable resolution.
- At runtime init (zephyrbt_thread_func), a lua_State is created per BT
  instance using k_malloc-backed allocator. User scripts are loaded
  first (strong), then the generated script (weak). Function names are
  resolved to Lua registry refs via lua_getglobal + luaL_ref. Only
  base, math, and string libraries are loaded (no io/os to avoid POSIX
  dependencies on bare-metal targets).
- The bb userdata is stored as a registry ref and passed as argument to
  each condition function call, rather than set as a global.
- zephyrbt_evaluate() calls pre-condition checks before the node tick
  and post-condition scripts after. The _while condition is re-evaluated
  when a node returns RUNNING.

The feature is fully optional behind
CONFIG_ZEPHYR_BEHAVIOUR_TREE_LUA_CONDITIONS which depends on NODE_INIT,
NODE_CONTEXT, DYNAMIC, and LUA (from lua_zephyr).

CMake API for user override files:
  zephyrbt_add_lua_file(app src/my_conditions.lua)

Changes:
- west.yml: Add lua_zephyr dependency (rodrigopex/lua_zephyr)
- subsys/zephyrbt/Kconfig: Add LUA_CONDITIONS and LUA_HEAP_SIZE options
- include/zephyr/zephyrbt/zephyrbt.h: Add condition enums,
  node_conditions struct, lua_cond_ref descriptor, bb_name_entry,
  context fields (bb_ref, lua_gen_script, lua_user_scripts), and
  ZEPHYRBT_DEFINE macro extension
- subsys/zephyrbt/zephyrbt_lua.c: Lua runtime with k_malloc allocator,
  bb bridge as registry ref, script loading (user then gen), function
  name resolution, pre_check, post_check, while_check
- subsys/zephyrbt/zephyrbt.c: Pre/post hooks in zephyrbt_evaluate(),
  Lua init call in zephyrbt_thread_func()
- scripts/generate-zephyrbt-from-behaviourtreecpp-xml: Separate
  condition attrs in parseNodes(), generate {name}_gen.lua with weak
  functions, generate lua_cond_ref[] with function names in _data.c
- cmake/zephyrbt-from-behaviourtreecpp-xml.cmake: Embed generated .lua
  via luaz_gen.py, add zephyrbt_add_lua_file() for user overrides
- samples/subsys/zephyrbt/lua_conditions/: Sample exercising _skipIf,
  _successIf, _failureIf, _onSuccess, _onFailure, _post with pytest.
  Targets native_sim/native/64 and mps2/an385. Includes POSIX stubs
  for bare-metal targets.

Tested on native_sim/native/64.

Fixes #45

Signed-off-by: Gerson Fernando Budke <gerson.budke@ossystems.com.br>
Demonstrates a ROS-like mobile robot patrol on an 8x8 grid using all
major ZephyrBT features: subtrees with port aliasing, user Lua override
files, shared C/Lua blackboard, and real computation in action nodes.

World and gameplay:
- 8x8 grid with obstacles, charger, home, and four patrol waypoints
- Robot navigates via BFS pathfinding, scanning a 3x3 area at each
  waypoint for targets; battery drains 1/step and 5/scan
- When battery < 20 %: emergency walk to charger, gradual recharge
  (+5 per 2 ticks) until 100 %, then patrol resumes
- After all four waypoints: robot returns home, celebrates five ticks
  ("Mission N complete!"), starts a new random mission
- Live ANSI terminal UI redraws an 8x8 grid each tick with colored
  symbols (R robot, C charger, H home, W waypoint, v visited,
  # obstacle, * target) and a battery bar

Behaviour tree architecture (Nav2-style reactive Fallback):
  Fallback
  +-- HandleEmergency [battery < critical -> charger]
  +-- HandleEmergency [obstacle < clearance -> safe pos]
  +-- Sequence "patrol"
  |   +-- Sense / Respawn / Render  (split UpdateWorld)
  |   +-- SelectWaypoint            (Lua gates: patrol_done, at_home)
  |   +-- NavigateTo [patrol]       (BFS + step-by-step, port alias)
  |   +-- PerformScan               (4-phase state machine)
  |   +-- NavigateTo [home]         (port alias, shared pos/battery)
  +-- AlwaysSuccess "idle"

Port aliasing provides per-instance blackboard variables for both
HandleEmergency subtrees (battery vs. obstacle metrics) and the two
NavigateTo instances (separate nav_status, shared robot position).

User Lua files (strong overrides before generated weak defaults):
  diagnostics.lua  – manhattan helper, per-mission counters
  navigation.lua   – waypoint selection and routing predicates
  safety.lua       – battery threshold estimation
  mission.lua      – scan tracking, progress, idle trigger

C nodes: BFS flood-fill pathfinding, step-by-step path following
(RUNNING until destination), battery drain model, 3x3 area sensor
sweep, nearest-obstacle Manhattan distance, waypoint list cycling,
and metric-vs-threshold comparison for emergencies.  Per-instance
path storage is keyed on the nav_status blackboard item pointer to
prevent path clobbering between NavigateTo subtree instances.

Test mode (CONFIG_ROBOT_PATROL_TEST_MODE): display_render emits
LOG_INF lines; BT delay overridden to 1 ms for fast CI execution.
sample.yaml ordered regex patterns verify the complete lifecycle:
home start → W1–W4 navigation and scan → battery emergency → charge
→ patrol resume → "Mission 1 complete!" → new mission start.

Engine changes included:
- scripts: apply_port_mapping() resolves both Lua condition scripts
  and normal attributes during subtree expansion
- scripts: addBlackboardVariables() processes all node attributes
  (was first only), fixing variable creation for expanded subtrees
- scripts: get_cond_func_name() includes node index for subtree
  instance disambiguation
- zephyrbt_lua.c: add weak zephyrbt_lua_user_init() hook for
  wiring user Lua scripts before Lua state initialization
- zephyrbt.h: lua_user_script_count made non-const for runtime
  setting via the user init hook

Signed-off-by: Gerson Fernando Budke <gerson.budke@ossystems.com.br>
@nandojve nandojve force-pushed the lua/add_initial_support branch from c0d7fdc to 1483f51 Compare April 17, 2026 15:48
@otavio otavio merged commit a5c5537 into master Apr 19, 2026
3 checks passed
@otavio otavio deleted the lua/add_initial_support branch April 19, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Add LUA script to process Pre/Post conditions

2 participants