This document is the single source of truth for all planned refactoring work on the peregrine_core stack, organized by priority and phase. It combines the original architecture refactoring guide with findings from a comprehensive file-by-file code audit.
These are correctness and safety bugs that should be fixed before any new feature work.
- Status: Superseded — completion detection was removed from generators entirely. Generators are now pure reference sources that output
(setpoint, progress). The trajectory_manager action server usesprogress >= 1.0to succeed goals. No position-based completion exists anywhere in this layer; the orchestrator (BT or demo script) owns that decision if needed. - Removed
completedanddistanceRemainingfromTrajectorySample - Removed
currentStateparameter fromsample()— generators no longer inspect vehicle state - All generators report progress [0,1] based on planned time/motion only
- trajectory_manager action server succeeds goals on
progress >= 1.0
- Status: Resolved by Phase 1.3 — emergency auto-clear was removed entirely. Recovery now requires explicit
ClearEmergencyservice call. - Resolved (auto-clear removed in Phase 1.3)
- Package:
trajectory_manager—trajectory_manager_node.cpp,publishTrajectorySetpoint() - Problem:
dataMutex_is held for 78+ lines covering trajectory sampling, setpoint construction, feedback publishing, AND goal resolution (succeed()/abort()calls). At 50Hz, subscriber callbacks can be blocked for the entire cycle. - Fix: Split into three phases:
sampleActiveTrajectory()— hold lock, copy state + sample resultemitSetpointAndFeedback()— no lock, publish setpoint and feedbackresolveGoalCompletion()— no lock, call succeed/cancel on goal handles
- Refactor
publishTrajectorySetpoint()into the three phases above - Verify no data races after refactor (goal handle access, generator swap)
- Package:
trajectory_manager—trajectory_manager_node.cpp - Problem: Generator is created in
onExecuteGoal()for validation, then created again inonExecuteAccepted()with a differentthis->now()and potentially differentlatestState_. Vehicle state can change between the two calls. - Fix: Cache the validated generator or the validated state snapshot during goal evaluation, reuse in
onExecuteAccepted(). - Store validated generator in a pending slot during
onExecuteGoal() - Consume it in
onExecuteAccepted()instead of recreating
Prepare uav_manager for a Behavior Tree application layer by removing orchestration logic and making it a pure state machine + safety gateway.
- Problem:
uav_managerproxiesGoToandExecuteTrajectoryactions by forwarding them totrajectory_manager. The BT will calltrajectory_managerdirectly. - Keep:
Takeoff,Land,Arm(these are state transitions the FSM must own). - Delete
forwardGoTo()(~87 LOC) - Delete
forwardExecuteTrajectory()— renamed toforwardTakeoffTrajectory()(internal use only for takeoff) - Remove the GoTo and ExecuteTrajectory action servers from
uav_manager - Remove the corresponding action client members (kept trajectoryExecuteClient_ for internal takeoff)
- Audit GoTo.action — still used by trajectory_manager (serves GoTo directly) and safety_regression_demo. Removed
acceptance_radius_mfield; feedback changed fromdistance_remaining_mtoprogress.
- Delete
action_orchestrator.cppandaction_orchestrator.hpp— dead code after forwarding removal - Rename types to
step_result.hpp(StepCodeenum +StepResultstruct — still used bycallArmService/callSetModeService) - Remove
action_orchestrator.cppfromCMakeLists.txt - Add
recoveryLand()touav_manager— sends PX4 land mode when takeoff fails with vehicle armed - Fix takeoff
failGoalto callrecoveryLandinstead of leaving quad armed and airborne - Fix land auto-disarm timeout: treat as landed (not as failure → hovering) since PX4 land mode is already active
- Problem: Forces PX4 into
POSCTLwhen nav state would prevent arming. This hides bad state from the caller. - Fix: If PX4 rejects arm,
uav_managermust fail the action. Let the BT handle recovery. - Delete
ensureArmableMode()and its call in the takeoff sequence - Update
Takeoffaction to return a clear error when PX4 rejects arm due to nav state - Document expected pre-arm states in the action definition or README
- Problem: 5-second automatic failsafe clearance. Emergencies are latched faults — automatic clearance is dangerous.
- Fix: Expose a
ClearEmergencyROS service. Operator or BT must explicitly clear. - Remove the 5-second hold timer and auto-clear logic from
publishUavState() - Add
ClearEmergency.srvtoperegrine_interfaces - Implement the service in
uav_manager(firesEmergencyClearedevent through the FSM) - Ensure the
TransitionGuardstill enforcesEmergencyClearReadyconditions (guard unchanged, evaluated via applyEvent)
- Problem: Takeoff and Land action servers repeat identical scaffolding: lifecycle gate, emergency check, action slot reservation, RAII release guard, preempted/emergency lambdas.
- Fix: Create a
create_guarded_action_server<T>()template or factory wrapper. - Note: With only 2 action servers remaining after 1.1, the ROI is lower. Consider deferring unless more actions are added for the BT layer.
- Design the guarded action server interface (template on action type + orchestration callable)
- Extract common rejection logic (lifecycle, emergency, slot reservation) into the wrapper
- Migrate Takeoff and Land to use it
- Problem:
callArmService()andcallSetModeService()(~90 LOC each) implement identical patterns: wait for service availability with 200ms poll + emergency check, then poll response future with 50ms intervals. - Fix:
template<typename SrvT> StepResult pollService(client, request, deadline, emergencyCheck). - Implement
pollService<T>()template - Migrate
callArmService()andcallSetModeService()
- Status: Complete — see Phase 5.3. All 7 nodes converted to
generate_parameter_library.
- Problem:
on_configure()blocks the lifecycle transition withwhile+sleep_for(100ms)waiting for PX4 topics. Appears incontrol_manager,estimation_manager, andtrajectory_manager. - Fix: Let
on_configure()returnSUCCESSinstantly. BroadcastWAITING_FOR_ESTIMATED_STATEvia status. Lock out flight operations until healthy. - Remove blocking loop from
control_manager::on_configure() - Remove blocking loop from
estimation_manager::on_configure() - Remove blocking loop from
trajectory_manager::on_configure() - Verify that each manager's status publisher correctly reports "waiting" when data is absent
- Verify that
uav_managerhealth aggregator correctly gates on manager readiness - Remove
data_readiness_timeout_sfromuav_manager— now waits indefinitely for all dependencies
- Problem: Locking
std::mutex250x/second to copy small telemetry messages.hardware_abstractionalready does this correctly withstd::atomic<std::shared_ptr<const Message>>. - Fix: Apply the same atomic shared_ptr pattern to
control_manager,trajectory_manager, andestimation_managersubscriber callbacks. - Audit which subscriber callbacks currently use mutex for simple message caching
- Convert to
std::atomic<std::shared_ptr<const T>>with acquire/release ordering - Remove the now-unnecessary mutexes (keep mutexes where multi-field consistency is required)
- Note:
trajectory_managermutex retained — protects generators, goal handles, and goal type (complex state, not simple caching)
- Problem:
on_deactivate()andon_error()are ~95% identical acrosscontrol_managerand likely other lifecycle nodes (cancel timers, deactivate publishers, set active=false). - Fix: Extract
void stopPublishing()private method. Apply across all lifecycle nodes. - Extract in
control_manager - Audit and extract in
trajectory_manager - Audit and extract in
estimation_manager
- Problem: 202-line function handling 4 control modes with 60-110 lines per branch.
- Fix: Extract
handleTrajectoryMode(),handleBodyRateMode(),handleAttitudeMode(),handleDirectActuatorMode(). - Extract the four mode handlers
- Keep frame validation as a shared preamble
- Package:
trajectory_manager - Problem: Hold generator is null until first
estimated_statearrives. Ifon_activate()fires first, there's a brief window with no hold generator. - Fix: Create a default hold at origin in
on_configure(). Overwrite when first state arrives. - Initialize
holdGenerator_with safe default inon_configure() - Document the fallback behavior
- Problem: 7 generators each override
name() const { return "hardcoded_string"; }via virtual dispatch. - Fix: Add
const std::string name_member to base class, set in constructor. Remove virtual method. - Add
name_member toTrajectoryGeneratorBase - Update all 7 generator constructors
- Remove virtual
name()override from each
-
generators.cpp: remove#include <limits> -
generators.hpp: remove#include <cstdint> -
rule_engine.cpp: remove#include <algorithm>
- Problem:
evaluateDetailed()(67 lines) mixes checker invocation with grace period state tracking (5 fields per rule, 3 conditional branches per cycle). - Fix: Extract
RuleStateclass withupdate(CheckResult, time_point)andisGraceExpired(). - Create
RuleStateclass - Migrate grace period logic out of
evaluateDetailed() - Add unit tests for grace period edge cases (deferred — logic is simple enough post-extraction)
- Package:
tui_status—tui_node.cpp - Problem: Three manager health callbacks (EST, CTL, TRJ) are structurally identical (~14 lines x3).
- Fix: Template helper or lambda factory parameterized by manager name.
- Extract helper
- Apply to all three manager status callbacks
- Problem: Manager container (4 composable nodes) is copy-pasted into 6+ launch files. Each example creates its own
ComposableNodeContainer. - Fix: One
core_stack.launch.pyinperegrine_bringup. All examples useIncludeLaunchDescription. - Create
core_stack.launch.pywith the full node composition - Accept YAML override paths as launch arguments
- Migrate all example launch files to use
IncludeLaunchDescription - Delete redundant container definitions (
peregrine_single_container.launch.py,example8_px4_sitl_single_uav.launch.py) - Delete
single_uav_sitl.launch.py— SITL is managed externally, not embedded in launch files - Update all references (
docker-compose.multi-sitl.yml,generate_multi_sitl.py,start_flight_stack.sh) fromsingle_uav.launch.pytocore_stack.launch.py - Convert
peregrine_bringupfromament_cmaketoament_python
- Problem: Separate SITL and non-SITL launch file variants for the same demo. SITL variants embed PX4/Gazebo startup that belongs in infrastructure, not examples.
- Fix: One launch file per demo, all include
core_stack.launch.py. SITL management is external. - Delete SITL example variants (
se3_circle_figure8_sitl.launch.py,se3_step_response_sitl.launch.py) - Delete dead tuning scripts (
se3_step_sweep.py,se3_circle_sweep.py) - Rename examples to descriptive names (drop
exampleN_prefix where done) - Update
examples/README.md
- Problem: 18 config files in examples, many differing by 1 field or duplicating package defaults.
- Fix: Delete redundant configs, consolidate SE3 gains into
control_manager, simplify example overrides. - Delete
example10_managers.yaml,example11_managers.yaml,example16_managers.yaml(only setstatus_rate_hz: 10.0, package default of 5Hz is fine) - Delete
safety_diag_only.yaml,safety_land_enabled.yaml(unreferenced) - Delete
circle_figure8_se3_mission.yaml,example10_se3_managers.yaml(redundant with consolidated SE3 configs) - Move SE3 gains to
control_manager/config/se3_tuned.yamlandse3_conservative.yaml - Rename
step_response_se3_mission.yaml→step_response_mission.yaml - Update launch files:
circle_figure8_demoandmulti_cycle_demouse package defaults, controller switch demos loadse3_tuned.yamlfromcontrol_manager - Retarget all demo script action clients from
uav_manager/*totrajectory_manager/*(circle_figure8, controller_switch, controller_switch_inflight, multi_cycle, step_response, safety_regression) - Fix
step_response_demo.pystep_sequenceparameter parsing (string-vs-list bug) - Update
controller_switch_demo.launch.pyandcontroller_switch_inflight_demo.launch.pyto passuav_params_filethrough tocore_stack
- Problem: Every demo script repeats: parameter declarations, UAVState subscription, action client setup, server wait logic, preflight readiness checks. ~60% of each script is identical boilerplate.
- Fix:
PeregrineClientclass encapsulating all ROS 2 client logic. - Create
peregrine_clientPython package - Implement core methods:
arm(),takeoff(alt),land(),execute(),go_to(),wait_ready(),clear_emergency(),set_mode() - Handle action client lifecycle internally (server wait, goal send, result polling)
- Provide async variants for BT integration
- Rewrite
circle_figure8_demo.pyusing the client (proof of concept) - Migrate
multi_cycle_demo.py,controller_switch_demo.py,controller_switch_inflight_demo.py,step_response_demo.py - Migrate remaining safety test scripts (
safety_regression_demo,safety_takeoff_hold_demo,safety_fault_injector)
- Replace
/opt/PX4-Autopilotwithpx4_autopilot_dirlaunch argument (default/opt/PX4-Autopilot) inmulti_uav_sitl.launch.py -
/tmp/circle_eval.jsonand/tmp/step_response_eval.jsonare already ROS parameters (output_path) — overridable via--ros-args
- Problem: Accumulated parameter surface had dead params, topic-name params that duplicate ROS 2 remapping, hardcoded physical constants exposed as tunables, MAVLink-era cargo params, and code-default/YAML-default drift.
- Audit scope: Every
declare_parametercall across all C++ nodes, cross-referenced againstdefaults.yamlfiles and actual usage. - Remove dead
home_init_timeout_sfromframe_transforms(declared, stored, never read) - Remove dead
gps_freshness_timeout_sfromframe_transforms(declared, stored, never read) - Remove dead
dependency_startup_timeout_sfromuav_manager(only referenced in validation, never used for timeout logic — leftover from Phase 2.2 blocking-wait removal) - Remove topic name params from
safety_monitor(battery_topic,gps_status_topic,estimated_state_topic,px4_status_topic,map_frame) — hardcoded defaults, use ROS 2 remapping if needed - Hardcode
se3.gravityto 9.81 — physical constant, not a tunable - Fix
se3.masscode default drift (was 1.5 in code vs 2.0643 in YAML) - Fix
se3.max_thrust_Ncode default drift (was 29.43 in code vs 34.19432 in YAML) - Remove MAVLink
target_component_id,source_system_id,source_component_idfromhardware_abstraction— cargo from MAVLink era, hardcoded to 1 - Re-add
target_system_idas a parameter (default 1) — PX4 Commander validatestarget_systemagainstMAV_SYS_IDeven over uXRCE-DDS; multi-instance SITL setsMAV_SYS_ID = instance+1, so hardcoding to 1 silently rejects commands for instances > 0 - Remove
home_init_timeout_sfrom bringup YAML configs (default.yaml,simulation.yaml) - Remove
dependency_startup_timeout_sfromsimulation.yaml - Remove
se3.gravityfrom all SE3 YAML configs (defaults.yaml,se3_tuned.yaml,se3_conservative.yaml) - Deferred: GPS quality threshold consolidation between
frame_transformsandsafety_monitor(same defaults, different nodes) — defer to Phase 5.3 (generate_parameter_library) - Deferred: uav_manager polling timeout params (
service_wait_s,orchestrator_poll_ms, etc.) — these are actively used in poll loops; the poll-loop architecture itself should be replaced with async ROS 2 patterns (Phase 1.5 or BT layer)
- Status: Complete. GoTo is actively used —
trajectory_managerserves the GoTo action directly, andsafety_regression_demo.pyuses it.uav_managerforwarding was removed in Phase 1.1.acceptance_radius_mwas removed from the action definition; feedback changed fromdistance_remaining_mtoprogress. - Grep for GoTo usage across the codebase
- GoTo is used by
trajectory_manager(serves directly) andsafety_regression_demo.py - Removed
acceptance_radius_mfield and updated feedback (Phase 1.1)
- Status: Evaluated — both custom messages are justified and kept.
-
PX4Status.msg— consolidates PX4-specific fields (nav_state,arming_state,failure_detector_status,motor_output) from multiple px4_msgs into one message. No standard ROS equivalent exists; replacing would leakpx4_msgsinto the stack or require multiple messages. -
GpsStatus.msg—sensor_msgs/NavSatFixlackshdop,vdop,eph,epv,satellites_usedwhichsafety_monitorandframe_transformsneed for GPS health gating. Replacing would require NavSatFix + a supplementary message for no benefit. - Both are thin translation layers published by
hardware_abstraction, consumed bysafety_monitor,tui_status,frame_transforms. Custom messages are justified.
- Status: Complete. All 7 nodes converted. Parameter YAML schemas are now the single source of truth. Clean Docker build verified (15/15 packages, 0 errors).
- Add
generate_parameter_libraryto all Dockerfiles (ros-${ROS_DISTRO}-generate-parameter-library) - Convert
safety_monitorparameters (33 params) - Convert
uav_managerparameters (9 params) - Convert
control_managerparameters (5 node-level params; SE3 controller usesdeclareOrGetplugin pattern, stays manual) - Convert
trajectory_managerparameters (4 params) - Convert
estimation_managerparameters (4 params) - Convert
hardware_abstractionparameters (7 params) - Convert
frame_transformsparameters (14 params) - Delete per-package
defaults.yaml— schema files become the source of truth
-
frame_transforms: addgeodeticToEnu()test with known GPS coordinates -
trajectory_manager: add generator progress tests (verifyprogressreaches 1.0 at expected time for each generator type) -
safety_monitor: add rule engine grace period unit tests (per Phase 3)
- Add a decision matrix: when to use single-container vs two-container vs three-container
- Document the temporal hierarchy (hard RT, soft RT, reflexes, BT) in
docs/ARCHITECTURE.md - Document the FSM vs BT separation of concerns in
docs/ARCHITECTURE.md
- Problem:
docker/config/tmuxinator/flight.ymluses tmux panes to run ROS 2 nodes. Crashed nodes leave dead terminals, break lifecycle tracking, and make logging unreliable. TUI on hardware wastes resources — monitoring belongs on GCS. - Fix for development (SITL): Keep tmuxinator (
peregrine.yml), it's fine for developers. - Fix for hardware (Jetson/RPi5): Container runs
start_flight_stack.shdirectly →ros2 launch core_stack.launch.py. Flight stack is PID 1 — Docker handles restart on crash. Debugging viamake shell-jetson/make shell-rpi5. - Delete
docker/config/tmuxinator/flight.yml - Update Jetson/RPi5 docker-compose
commandto runstart_flight_stack.shdirectly - Remove
ruby-fullandgem install tmuxinatorfrom Jetson/RPi5 Dockerfiles - Remove tmuxinator config COPY/setup from Jetson/RPi5 Dockerfiles
- TUI stays on GCS only (already in
gcs.generated.yml) - Logs: ROS 2
~/.ros/log/+docker logs— no tmux scrollback needed - Hardware debugging:
make shell-jetson/make shell-rpi5→docker exec aircraft bash
The BT becomes the "pilot" — it reads UAV state, decides intent, and ticks actions. It does NOT hold flight state (the FSM in uav_manager does that). The integration is split across Phases 7-10 to make scope and dependencies explicit.
Prerequisite: Phases 0-1 must be complete. Phase 2 strongly recommended (removes blocking anti-patterns that would interfere with BT tick timing).
- Status: Complete. Package created with BT.CPP v4 + official
behaviortree_ros2bridge. - Library: BehaviorTree.CPP v4 — the de facto standard for ROS 2 BT applications.
- Bridge: BehaviorTree.ROS2 — official ROS 2 integration by the BT.CPP author. Provides
RosActionNode<T>,RosServiceNode<T>,RosTopicSubNode<T>,RosTopicPubNode<T>base templates. Not available via apt for Humble — cloned as a git submodule insrc/behaviortree_ros2. - Process isolation: The BT executor runs as a separate process, not a composable node in the core stack container. See
docs/ARCHITECTURE.mdfor rationale. Launch integration comes in Phase 9.5 viabt_mission.launch.py. - Add
behaviortree_cppas a dependency (available viaapton Humble) - Add
behaviortree_ros2as a git submodule insrc/(not on apt for Humble) - Create
peregrine_btpackage withCMakeLists.txt,package.xml - Delete empty
mission_executorskeleton (replaced byperegrine_bt) - Verify BT.CPP v4 (4.9.0) builds against ROS 2 Humble
- Verify the package builds in the Docker simulation image (19 packages, 0 errors)
- Status: Superseded — using the official
behaviortree_ros2package instead of custom templates. The official library providesRosActionNode<T>,RosServiceNode<T>,RosTopicSubNode<T>with client reuse via static registry, proper error enums, callback group isolation, and non-blocking async execution. -
RosActionNode<T>— provided bybehaviortree_ros2 -
RosServiceNode<T>— provided bybehaviortree_ros2 -
RosTopicSubNode<T>— provided bybehaviortree_ros2 - Blackboard conventions — not needed; the official library uses typed input/output ports per node and
default_port_valueinRosNodeParamsfor topic/action/service names
- Deferred until mission trees are implemented and testable in SITL (Phase 9/10). The
behaviortree_ros2TreeExecutionServerhas built-in Groot2 publisher support. - Verify Groot2 compatibility with BT.CPP v4 version
- Enable Groot2 publisher in the BT executor node
- Document how to connect Groot2 to a running BT for live visualization
Each BT node is a thin adapter between the tree and a specific ROS 2 interface. No business logic — if a node needs more than ~30 lines beyond the base template, the complexity likely belongs in the ROS 2 server, not the BT node. All nodes use the official behaviortree_ros2 base templates.
- Status: Complete. 15 condition nodes implemented as
RosTopicSubNode<T>subclasses.
-
IsArmed— readsUAVState.armed -
IsFlying— readsUAVState.state == FLYINGorHOVERING -
IsLanded— readsUAVState.state == IDLEorLANDED -
IsDependenciesReady— readsUAVState.dependencies_ready -
IsEmergency— readsUAVState.state == EMERGENCY(added — needed for reactive tree guards) -
IsConnected— readsUAVState.connected(added — needed to detect PX4 disconnect) -
IsOffboard— readsUAVState.offboard(added — useful pre-trajectory check)
-
IsSafetyNominal— readsSafetyStatus.level == NOMINAL -
IsSafetyAtLeast(max_level)— returns SUCCESS iflevel <= max_levelinput port
-
IsBatteryAbove(threshold_pct)— readsPX4Status.battery_remaining(subscribes tostatus) -
IsGpsHealthy(min_fix_type, max_hdop, max_vdop)— reads fix type, HDOP, and VDOP (subscribes togps_status). MergedIsGpsHdopBelow— separate node was unnecessary.
-
HasValidState(max_age_s)— checks estimated state freshness via header timestamp -
IsAtPosition(target_x, target_y, target_z, tolerance_m)— 3D Euclidean distance check -
IsAboveAltitude(min_alt_m)— checkspose.position.z -
IsBelowAltitude(max_alt_m)— checkspose.position.z
- Status: Complete. 4 action nodes, 3 service nodes, 1 utility node.
-
ArmService— callsarmservice viaRosServiceNode<Arm>(onhardware_abstraction) -
TakeoffAction(altitude_m, climb_velocity_mps)— callsuav_manager/takeoffaction, output portfinal_altitude_m -
LandAction(descent_velocity_mps)— callsuav_manager/landaction
-
ExecuteTrajectoryAction(trajectory_type, params)— callstrajectory_manager/execute_trajectory. Params passed as comma-separated string, parsed tofloat64[]. -
GoToAction(x, y, z, yaw, velocity_mps)— callstrajectory_manager/go_to(added — GoTo is an actively used action, was missing from original TODO)
-
SetModeService(mode)— callsset_modeservice (onhardware_abstraction) -
ClearEmergencyService— callsuav_manager/clear_emergencyservice
-
WaitAction(duration_s)—StatefulActionNodewith ROS clock deadline (sim-time compatible) — dropped; BT.CPP v4 built-inLogActionStdCoutLoggerand script nodes cover this— dropped; BT.CPP v4 has a built-inSetBlackboardActionSetBlackboardnode
- Status: Complete. All 23 nodes registered in
register_nodes.cppwith default topic/action/service names viaRosNodeParams. - All nodes registered with
BT::BehaviorTreeFactoryusingregisterNodeType<>() - Registrations organized in
register_nodes.cppwith defaultRosNodeParamsper node - Add
BT_REGISTER_NODESmacro export for plugin-based loading (deferred — not needed until user-defined BT nodes)
- Status: Complete. Lifecycle node with tick timer, shared client node, and
MultiThreadedExecutor. - Create
BTExecutorNodeas a lifecycle node - Parameter:
tree_file(string) — path to the XML tree to execute - Parameter:
tick_rate_hz(double, default 2.0) — BT tick frequency - On
on_configure(): load XML, register all node types viaregisterAllNodes(), create tree - On
on_activate(): start the tick timer - On
on_deactivate(): halt the tree, cancel tick timer - Graceful shutdown:
on_shutdown()halts tree and cleans up - Publish tree status on
bt_statustopic each tick (current state of root, number of RUNNING nodes, active action names)
- Status: Complete. Separate
rclcpp::Node("bt_client") created in the executor constructor, shared to all BT nodes viaRosNodeParams. Both nodes added to aMultiThreadedExecutor. - Single shared
rclcpp::Nodepassed to all BT nodes viaRosNodeParams - The
behaviortree_ros2library handles subscription/client reuse via static registries — multiple nodes subscribing to the same topic share one subscription -
MultiThreadedExecutorspins both the lifecycle node and the client node - Forward
use_sim_timefrom the lifecycle node to the client node — required for correctnode->now()in SITL (affectsHasValidStatetimestamp comparison andWaitActiondeadline)
Start with simple trees that replicate existing demo scripts, then build up complexity. Each tree file lives in peregrine_bt/trees/.
- Status: Complete. Sequence: IsDependenciesReady → TakeoffAction → WaitAction 10s → LandAction.
- Tree XML implemented
- Validate in SITL
- Verify BT status topic shows correct state transitions
- Status: Complete. ReactiveFallback with safety guard around mission sequence, safety land fallback.
- Tree XML implemented
- Validate in SITL
- Test: trigger a geofence warning mid-circle, verify BT switches to safety land
- Status: Complete. Uses
PreflightAndTakeoffsubtree, RetryNode(3) for each trajectory. - Tree XML implemented with reusable
PreflightAndTakeoffsubtree - Validate in SITL — verify retry on trajectory failure
- Status: Complete. Sequence: IsDependenciesReady → TakeoffAction → SetModeService(se3) → circle → SetModeService(passthrough) → LandAction.
- Tree XML implemented
- Validate in SITL — verify controller switch mid-flight
- Status: Complete. Two reusable subtrees in
peregrine_bt/trees/subtrees/. -
PreflightAndTakeoff— IsDependenciesReady → IsBatteryAbove → IsGpsHealthy → IsSafetyNominal → TakeoffAction with{altitude_m}blackboard port -
PreflightChecks— same checks without takeoff (standalone preflight gate) - Stored in
peregrine_bt/trees/subtrees/
- Status: Complete.
bt_mission.launch.pycreated inperegrine_bringup. - Create
bt_mission.launch.py: includescore_stack.launch.py(optional viastart_core_stackarg), launchesBTExecutorNodeas aLifecycleNode(separate process) - Launch arguments:
tree_file(required),tick_rate_hz(default 2.0),start_core_stack(default true — set false when stack is already running on hardware) -
core_stack.launch.pyunchanged — all its args pass through frombt_mission.launch.py
The BT operates at the slowest layer (1-10Hz). It must NOT:
-
Bypass the FSM by sending raw PX4 commands
-
Hold references to high-frequency data (subscribe, snapshot, release)
-
Block on long computations during tick (offload to async action nodes)
-
Assume tick timing is deterministic (use timeouts, not tick counts)
-
All action nodes verified async (return RUNNING, never block tick thread)
-
All condition nodes verified non-blocking (read cached subscriber values via
behaviortree_ros2registry, not per-tick subscriptions) -
Document the tick rate contract: "BT is safe to stutter or pause without affecting flight safety — the FSM and safety_monitor handle real-time concerns"
-
Measure worst-case tick duration under load — must stay under 50ms at 2Hz tick rate
- Test each condition node in isolation: mock ROS topic data, verify SUCCESS/FAILURE thresholds
- Test each action node in isolation: mock ROS action/service server, verify RUNNING→SUCCESS and RUNNING→FAILURE paths
- Test condition nodes return FAILURE when no message has been received (null message path)
- Full stack smoke test: SITL + core_stack + BT executor, run Tree 1, verify takeoff/hover/land
- Safety fallback test: run Tree 2, inject geofence violation mid-flight, verify reactive land
- Retry test: run Tree 3, kill trajectory_manager briefly, verify retry succeeds after restart
- Controller switch test: run Tree 4, verify SE3 ↔ passthrough switching in flight
- Multi-UAV test: two UAVs each running independent BT trees, verify no cross-talk
- Kill
safety_monitormid-flight — verify BT's reactive fallback triggers land (safety status goes stale → IsSafetyNominal returns FAILURE) - Kill
trajectory_managermid-trajectory — verify action node returns FAILURE, tree handles it (retry or land) - Kill
uav_managermid-takeoff — verify BT detects loss of UAV state, triggers fallback - Simulate PX4 disconnect (stop the SITL instance) — verify stack degrades gracefully, BT does not hang on RUNNING forever
- Tick starvation: artificially slow the BT tick rate to 0.5Hz, verify flight safety is unaffected (FSM and safety_monitor handle RT concerns independently)
- Groot2 integration verified: connect to running BT, visualize live tree state
- Groot2 recording: capture tree execution traces for post-flight analysis (log to file)
- TUI integration: add BT status panel to
tui_statusshowing current tree state, active action, tick rate -
bt_statustopic documented: message format, update rate, how to interpret states
-
peregrine_bt/README.md: package overview, how to write a custom tree, how to add new BT nodes - Document the FSM vs BT separation of concerns — see
docs/ARCHITECTURE.md - Document the temporal hierarchy — see
docs/ARCHITECTURE.md - Add example: "How to create a new mission" walkthrough (copy a tree XML, customize parameters, launch)
Add command capability to the TUI so operators can send basic flight commands from the GCS terminal. Currently the GCS is read-only (Zenoh bridges only carry topics UAV → GCS). This phase opens the reverse path for actions and services.
Open the existing Zenoh bridges to carry actions and services GCS → UAV. No new bridges or containers needed.
-
uav_bridge.json5: addaction_serversentries (".*/uav_manager/takeoff",".*/uav_manager/land") -
uav_bridge.json5: addservice_serversentries (".*/arm",".*/set_mode",".*/uav_manager/clear_emergency") -
generate_gcs_config.py: add matchingaction_clientsandservice_clientsto the GCS bridge template - Regenerate
gcs_bridge.generated.json5 - Verify round-trip: GCS action client → Zenoh → UAV action server (test with
ros2 action send_goalfrom GCS container)
Add action and service clients to tui_node. The TUI already has the correct uav_namespace parameter — clients resolve under the same namespace.
- Add action clients:
uav_manager/takeoff,uav_manager/land - Add service clients:
arm,set_mode,uav_manager/clear_emergency - Non-blocking dispatch: send goal/request, track status, never block the TUI render loop
- Command timeout handling (Zenoh latency + server responsiveness)
- Keybindings:
T=takeoff,L=land,A=arm/disarm,M=cycle mode,E=clear emergency - Confirmation for destructive commands (double-press or shift key for arm/takeoff)
- Command status line in footer: show pending command, result (SUCCESS/FAILURE/TIMEOUT)
- Update footer help text with new keybindings
| Phase | Estimated Effort | Priority |
|---|---|---|
| Phase 0: Safety fixes | 2-3 days | Immediate |
| Phase 1: uav_manager strip-down | 3-4 days | High |
| Phase 2: C++ de-sloping | 1-2 weeks | High |
| Phase 3: safety_monitor improvements | 2-3 days | Medium |
| Phase 4: Launch & Python overhaul | 1-2 weeks | Medium |
| Phase 5: Interface & architecture cleanup | 3-4 days | Medium |
| Phase 6: Production readiness | 2-3 days | Before hardware flight |
| Phase 7: BT package scaffolding & bridge | After Phases 0-2 | |
| Phase 8: BT node implementation | After Phase 7 | |
| Phase 9: Mission trees & executor | After Phase 8 | |
| Phase 10: BT testing & operational readiness | 1-2 weeks | After Phase 9 |
| Phase 11: TUI command interface | 2-3 days | Optional / after Phase 10 |