Reference Implementation: group0_final#
This page is an API reference for the reference implementation in
~/enpm605_ws/src/final_project/group0_final/. It is provided as
an example of:
The level of docstring detail expected for the final project (Google-style, parsed by napoleon).
How to organize a ROS 2 Python package that combines a behavior tree (
py_trees/py_trees_ros), Nav2 action clients, custom services, TF broadcasting, and YAML-driven parameters.The recommended module layout that separates pure mission state (
ZoneManager), behavior-tree leaves (bt_nodes/), service servers (service_servers/), and the entry-point script (scripts/).
Use this as a style and structure reference, not a copy-paste template. Your own package will share the same shape but should be written in your own words.
Note
The behavior tree assembled by the entry point is:
Selector "Mission" (memory=False) <- root
|-- Sequence "Patrol" (memory=True)
| |-- ZonesRemaining? (condition)
| |-- NavigateToZone (action)
| |-- DetectSurvivor (action)
| |-- Selector "HandleDetection" (memory=False)
| | |-- Sequence "SurvivorFound" (memory=False)
| | | |-- IsSurvivorDetected? (condition)
| | | |-- BroadcastSurvivorTF (action)
| | | |-- NotifyBase (action)
| | |-- LogNoDetection (action)
| |-- AdvanceZone (action)
|-- NavigateToBase (action)
Mission State#
Mission-state container shared across behavior-tree nodes.
The ZoneManager is a plain Python class – not a Node and not a
Behaviour – that owns the ordered list of search zones and the
base-station pose. The single instance is injected into every BT node
that needs zone information, which lets the tree share state without
the py_trees blackboard.
- class group0_final.zone_manager.ZoneManager(zones, base_station)#
Bases:
objectTrack the active search zone, base-station pose, and survivor IDs.
- _zones#
Ordered list of zone dictionaries. Each dictionary must contain the keys
id(str),x(float),y(float), andyaw(float, radians).
- _base_station#
Dictionary with keys
x,y,yawfor the return-to-base pose.
- _index#
Index of the current zone within
_zones. When_index >= len(_zones), no zones remain.
- _survivor_count#
Number of survivors discovered so far. Used to generate unique TF child-frame and report IDs (
survivor_1,survivor_2, …).
- advance()#
Move the cursor to the next zone in the patrol list.
Calling
advancewhen no zones remain is a no-op.- Return type:
- base_pose()#
Return the base-station pose dict.
- current_index()#
Return the 0-based index of the current zone.
- Return type:
- Returns:
The index of the zone returned by
current_zone. Whenhas_remainingisFalsethis equalstotal_zones.
- current_zone()#
Return the dict of the zone currently being patrolled.
- Return type:
- Returns:
The current zone’s
{id, x, y, yaw}dictionary.- Raises:
IndexError – If the mission has already advanced past the last zone (callers should gate this with
has_remainingfirst).
- has_remaining()#
Report whether at least one unvisited zone is still queued.
- Return type:
- Returns:
Truewhilecurrent_zoneis safe to call;Falseonce every zone has been advanced past.
- next_survivor_id()#
Allocate and return a unique survivor identifier.
Increments an internal counter so successive calls return
"survivor_1","survivor_2", … – one ID per discovery.- Return type:
- Returns:
The newly allocated survivor ID string.
Behavior Tree Leaves#
Actions#
Action behaviours for the search-and-rescue tree.
This module collects every action leaf node used by the mission tree:
NavigateToZone/NavigateToBase– asyncNavigateToPoseaction clients that are non-blocking insideupdate().DetectSurvivor– synchronousdetect_survivorservice call that caches the result for downstream nodes.BroadcastSurvivorTF– publishes a staticmap -> survivor_NTF frame usingtf2_ros.StaticTransformBroadcaster.NotifyBase– synchronousreport_survivorservice call.AdvanceZone/LogNoDetection– bookkeeping helpers.
- class group0_final.bt_nodes.actions.AdvanceZone(name, zone_manager)#
Bases:
BehaviourMove the
ZoneManagercursor to the next zone.- _zone_manager#
Shared
ZoneManagerinstance.
- update()#
Advance the zone cursor unconditionally.
- Return type:
Status- Returns:
Always
SUCCESS.
- class group0_final.bt_nodes.actions.BroadcastSurvivorTF(name, detect_node, zone_manager)#
Bases:
BehaviourPublish a static
map -> survivor_NTF frame.The static broadcaster latches the transform on
/tf_staticso the frame stays valid for the rest of the mission even after the BT moves on.- _detect_node#
Reference to
DetectSurvivor(source of the survivor coordinates).
- _zone_manager#
Shared
ZoneManager(allocates survivor IDs).
- _node#
Owning ROS 2 node.
- _broadcaster#
StaticTransformBroadcasterinstance.
- last_survivor_id()#
Return the most recently allocated survivor frame ID.
- Return type:
- Returns:
The string used as
child_frame_idon the last broadcast (e.g."survivor_1"). Empty string if the node has not yet published any frame.
- setup(**kwargs)#
Create the static TF broadcaster.
- Return type:
- Parameters:
**kwargs – Must contain
'node'.
- update()#
Build and publish the
map -> survivor_Nstatic transform.- Return type:
Status- Returns:
SUCCESSaftersendTransformis called;FAILUREif no survivor pose is available (defensive – theIsSurvivorDetectedcondition should make this unreachable in practice).
- class group0_final.bt_nodes.actions.DetectSurvivor(name, zone_manager)#
Bases:
BehaviourCall
detect_survivorfor the current zone and cache the result.Returns
SUCCESSonce the service call completes – whether or not a survivor was found. The cached result is exposed viawas_foundandsurvivor_poseso sibling nodes can branch on it without re-issuing the service call.- _zone_manager#
Shared
ZoneManager.
- _node#
Owning ROS 2 node.
- _client#
Service client for
detect_survivor.
- _was_found#
Cached
foundflag from the last response.
- _survivor_xy#
Cached survivor coordinates, or
Nonewhen no survivor has been observed yet.
- SERVICE_NAME = 'detect_survivor'#
- SERVICE_TIMEOUT_SEC = 5.0#
- set_survivor_id(survivor_id)#
Record the survivor ID assigned to the current detection.
Called by
BroadcastSurvivorTFafter it allocates a freshsurvivor_Nframe name. Storing the ID on the detection node letsNotifyBasereuse the same string without holding a direct reference to the broadcaster.- Return type:
- Parameters:
survivor_id – Newly allocated survivor identifier (e.g.
"survivor_1").
- setup(**kwargs)#
Create the
detect_survivorservice client.- Return type:
- Parameters:
**kwargs – Must contain
'node'.
- survivor_id()#
Return the survivor ID assigned to the latest detection.
- Return type:
- Returns:
The string set by
BroadcastSurvivorTF, or an empty string when no broadcast has happened for the current zone.
- survivor_pose()#
Return the cached survivor coordinates, if any.
- update()#
Submit the detection request once and poll the future.
py_trees_ros drives
tick_tockfrom a timer that runs inside the executor that is already spinningtree.node. Callingrclpy.spin_until_future_completefrom here would re-enter that same executor and raiseExecutor is already spinning. Instead, we use the canonical async-BT pattern: submitcall_asynconce, returnRUNNINGuntilfuture.done(), then process the response in a single tick.- Return type:
Status- Returns:
RUNNINGwhile the service is unavailable or the response has not yet arrived;SUCCESSonce the call completes (regardless of whether a survivor was found);FAILUREif the service responds withNone.
- class group0_final.bt_nodes.actions.LogNoDetection(name, zone_manager)#
Bases:
BehaviourLog that no survivor was found at the current zone.
- _zone_manager#
Shared
ZoneManager(used for the log message).
- _node#
Owning ROS 2 node.
- setup(**kwargs)#
Cache the owning ROS 2 node.
- Return type:
- Parameters:
**kwargs – Must contain
'node'.
- update()#
Log a single info-level message and return
SUCCESS.- Return type:
Status- Returns:
Always
SUCCESS.
Bases:
_NavigateToPoseActionSend the robot to the base-station pose via Nav2.
Shared
ZoneManager(forbase_pose).
Tick the navigation, then announce completion exactly once.
The base class handles the Nav2
NavigateToPoselifecycle. We add a single one-shotMission complete.log when the goal succeeds so the operator can tell the mission is done.OneShotin the tree assembly prevents this branch from being re-ticked, but the flag is kept as a defensive guard in case the decorator is removed later.- Return type:
Status- Returns:
The status returned by the base
NavigateToPosepoll.
Bases:
_NavigateToPoseActionSend the robot to
zone_manager.current_zonevia Nav2.Shared
ZoneManager; queried eachinitialise.
- class group0_final.bt_nodes.actions.NotifyBase(name, detect_node)#
Bases:
BehaviourCall
report_survivorfor the survivor that was just found.Sends the survivor location as a
geometry_msgs/PointStampedin themapframe so the receiving server (and any downstream consumer) can interpret the coordinates without a TF lookup.- _detect_node#
DetectSurvivoraction node (source of both the survivor coordinates and the survivor ID, which the broadcaster stashed back onto it).
- _node#
Owning ROS 2 node.
- _client#
Service client for
report_survivor.
- SERVICE_NAME = 'report_survivor'#
- SERVICE_TIMEOUT_SEC = 5.0#
- setup(**kwargs)#
Create the
report_survivorservice client.- Return type:
- Parameters:
**kwargs – Must contain
'node'.
- update()#
Submit the report once and poll the future across ticks.
Same async pattern as
DetectSurvivor.update: blocking onrclpy.spin_until_future_completewould re-enter the executor that is already spinningtree.node, so we submit the request once and yieldRUNNINGuntilfuture.done().- Return type:
Status- Returns:
RUNNINGwhile the service is unavailable or the response has not yet arrived;SUCCESSonce the server setsacknowledged=True;FAILUREif the response isNoneoracknowledged=False, or if the survivor pose / ID has not been set by the upstream BT nodes.
Bases:
BehaviourCommon base class wrapping the asynchronous
NavigateToPoseclient.Subclasses provide the target pose for each tick by implementing
_target_pose. The base class handles goal submission, status polling, and cancellation in a way that keepsupdate()non-blocking (a hard requirement for a BT leaf).- Lifecycle:
setup: create theActionClient.initialise: send a goal asynchronously, reset state flags.update: poll for goal acceptance and result; returnRUNNINGwhile in flight,SUCCESSon Nav2 success,FAILUREon rejection / failure / cancellation.terminate: cancel any in-flight goal.
Short label used in log messages (e.g. zone id).
The owning
rclpynode.
ActionClientfornavigate_to_pose.
Handle returned by Nav2 once a goal is accepted.
Future for
send_goal_async.
Future for
get_result_async.
True once a terminal status has been observed.
Final outcome (only valid when
_doneis True).
Submit a fresh navigation goal at the start of an active period.
Resets the per-goal state and calls
send_goal_async. The future stored in_send_futureis polled byupdate.- Return type:
Acquire the ROS 2 node and create the
NavigateToPoseclient.
Cancel an in-flight Nav2 goal when the BT preempts the action.
- Return type:
- Parameters:
new_status – The status the BT is transitioning into. Used to skip cancellation when the goal already terminated successfully.
Poll the asynchronous goal/result futures.
- Return type:
Status- Returns:
RUNNINGwhile the goal is in flight,SUCCESSonce Nav2 reports success,FAILUREon rejection or any non-success terminal status.
- group0_final.bt_nodes.actions._make_pose_stamped(node, x, y, yaw, frame_id='map')#
Build a
PoseStampedin the requested frame.- Return type:
PoseStamped- Parameters:
node – ROS 2 node used to read the current clock.
x – Target x in metres.
y – Target y in metres.
yaw – Target heading in radians.
frame_id – Frame the pose is expressed in (default
"map").
- Returns:
A populated
geometry_msgs/PoseStamped.
- group0_final.bt_nodes.actions._yaw_to_quaternion(yaw)#
Convert a yaw angle (radians) into a unit quaternion (z-axis rotation).
- Return type:
Quaternion- Parameters:
yaw – Heading angle in radians.
- Returns:
geometry_msgs/Quaternionrepresenting the yaw rotation about the z-axis. Roll and pitch are zero.
Conditions#
Condition behaviours for the search-and-rescue tree.
Condition nodes return only SUCCESS or FAILURE (never
RUNNING). They give the parent composite an instantaneous yes/no
answer used to gate downstream actions.
- class group0_final.bt_nodes.conditions.IsSurvivorDetected(name, detect_node)#
Bases:
BehaviourReports the most recent
DetectSurvivorresult.Reads the result through a direct reference to the
DetectSurvivoraction node (constructor injection), avoiding the py_trees blackboard.- _detect_node#
Reference to the
DetectSurvivorBT action node whosewas_foundmethod is queried each tick.
- update()#
Return the cached result from the last detection call.
- Return type:
Status- Returns:
SUCCESSif the previous call reported a survivor was found at the current zone;FAILUREotherwise.
- class group0_final.bt_nodes.conditions.ZonesRemaining(name, zone_manager)#
Bases:
BehaviourSucceeds while the patrol still has unvisited zones.
Wraps
ZoneManager.has_remaining: returnsSUCCESSwhile the patrol list still has work,FAILUREonce every zone has been visited. The parentPatrol Sequenceuses this answer to decide whether to keep patrolling or fall through to the return-to-base branch.- _zone_manager#
Shared
ZoneManagerinstance whose state is queried on every tick.
- update()#
Check whether the patrol list still has zones to visit.
- Return type:
Status- Returns:
SUCCESSif at least one unvisited zone remains;FAILUREonce the patrol has been exhausted.
Service Servers#
Simulated survivor-detection service server.
This module implements a stand-in for a real perception system. Instead
of running computer vision, it answers DetectSurvivor requests with
a hardcoded lookup that maps zone IDs to known survivor coordinates in
the map frame.
The server does not check whether the robot is physically near the zone – the behavior tree is responsible for sequencing detection only after navigation succeeds.
- class group0_final.service_servers.detect_survivor_server.DetectSurvivorServer#
Bases:
NodeROS 2 node that advertises the simulated
detect_survivorservice.- _service#
The created ROS 2 service (
detect_survivor).
- _survivors#
The hardcoded zone -> (x, y) lookup table.
- group0_final.service_servers.detect_survivor_server.main(args=None)#
ROS 2 entry point for the detection service server.
- Return type:
- Parameters:
args – Forwarded to
rclpy.init(defaults tosys.argv).
Simulated command-center service server.
Receives ReportSurvivor requests from the behavior tree and pretends
to forward them to a remote command center. In reality the report is
just logged to the ROS 2 console. The server warns (but still
acknowledges) if a request arrives in a frame other than "map",
since the BT contract sends survivor positions in the global fixed
frame.
Entry Point#
Entry point: assemble and run the search-and-rescue behaviour tree.
The BT structure (composites + leaves):
Selector "Mission" (memory=False) <- root
|-- Sequence "Patrol" (memory=True)
| |-- ZonesRemaining? (condition)
| |-- NavigateToZone (action)
| |-- DetectSurvivor (action)
| |-- Selector "HandleDetection" (memory=False)
| | |-- Sequence "SurvivorFound" (memory=False)
| | | |-- IsSurvivorDetected? (condition)
| | | |-- BroadcastSurvivorTF (action)
| | | |-- NotifyBase (action)
| | |-- LogNoDetection (action)
| |-- AdvanceZone (action)
|-- NavigateToBase (action)
The Patrol Sequence uses memory=True so once NavigateToZone
returns RUNNING the tree does not restart navigation on the next
tick. The other composites are reactive (memory=False).