Theoretical Background: Autonomous Behaviour and Mission Execution
Module 6 Theory: Autonomous Behaviour and Mission Execution
Learning Objectives
- Understand the six-state
BehaviorModeFSM inautonomous_behavior.py - Analyse state transition logic and the role of sensor data in mode switching
- Study the wall-following and exploration algorithms at the behaviour level
- Understand the pick-and-place task FSM in
useMaterialHandling - Connect behaviour-based execution to the broader integrated system (6.2–6.4)
6.1.1 Behaviour-Based Architecture
ArbiterROS implements a behaviour-based architecture at the backend. Rather than a
single monolithic controller, autonomous_behavior.py defines six
discrete modes that are selected based on sensor state and operator commands. This
separates concerns cleanly: each behaviour encapsulates its own sensor processing
and control law, and the mode-switching logic is centralised and auditable.
BehaviorMode Enumeration:
# autonomous_behavior.py
class BehaviorMode(Enum):
IDLE = "idle"
MANUAL = "manual"
OBSTACLE_AVOIDANCE = "obstacle_avoidance"
FOLLOW_WALL = "follow_wall"
EXPLORE = "explore"
GO_TO_GOAL = "go_to_goal"
Configurable Parameters:
self.declare_parameter('behavior_mode', 'obstacle_avoidance')
self.declare_parameter('max_linear_speed', 0.5) # m/s
self.declare_parameter('max_angular_speed', 1.0) # rad/s
self.declare_parameter('obstacle_threshold', 0.5) # m
self.declare_parameter('goal_tolerance', 0.1) # m
self.declare_parameter('wall_follow_distance', 0.3) # m
self.declare_parameter('exploration_timeout', 30.0) # s
6.1.2 State Definitions and Transition Conditions
IDLE
The robot is stationary. No velocity commands are published. The node waits for a
mode-change message on /behavior_mode or a goal on
/move_base_simple/goal.
Transitions out:
/behavior_modemessage received set corresponding mode- Goal received on
/move_base_simple/goalgo_to_goal
MANUAL
The node forwards /cmd_vel commands from the web dashboard without
modification. No sensor processing or autonomous control is applied.
OBSTACLE_AVOIDANCE
Reactive avoidance using LiDAR sector analysis. The front sector is scanned
for obstacles within obstacle_threshold. When clear, the robot drives forward;
when blocked, it rotates away from the nearest obstacle.
def _obstacle_avoidance_control(self):
if self.laser_data is None:
return Twist()
ranges = self.laser_data.ranges front_idx = len(ranges) // 2 sector = 30 # degrees each side
Minimum distance in front arc
front = r for r in rangesfront_idx-sector:front_idx+sector if self.laser_data.range_min < r < self.laser_data.range_max min_front = min(front) if front else float('inf')
cmd = Twist()
if min_front > self.obstacle_threshold:
cmd.linear.x = self.max_linear_speed
else:
# Turn away from nearest obstacle
left_mean = self._sector_mean(ranges, front_idx+sector,
front_idx+2*sector)
right_mean = self._sector_mean(ranges, front_idx-2*sector,
front_idx-sector)
cmd.angular.z = self.max_angular_speed \
if left_mean > right_mean else -self.max_angular_speed
return cmd
FOLLOW_WALL
The robot maintains a fixed lateral distance from the nearest wall surface. A PD
controller drives the robot to keep the right-side sensor reading at
wall_follow_distance:
EXPLORE
Random-walk exploration with collision avoidance. The robot selects a random heading
when it encounters an obstacle or when the exploration_timeout elapses,
then drives in that direction until the next event.
def _explore_control(self):
now = self.get_clock().now().to_msg().sec
elapsed = now - self.exploration_start_time
cmd = Twist()
if (self._is_path_clear() and
elapsed < self.exploration_timeout):
cmd.linear.x = self.max_linear_speed * 0.7
else:
# Choose new random heading
import random
cmd.angular.z = random.uniform(
-self.max_angular_speed, self.max_angular_speed)
self.exploration_start_time = now
return cmd
GO_TO_GOAL
Proportional heading controller driving to a geometry_msgs/Point goal
received on /move_base_simple/goal:
def _go_to_goal_control(self):
if self.goal_point is None or self.robot_pose is None:
return Twist()
dx = self.goal_point.x - self.robot_pose.x dy = self.goal_point.y - self.robot_pose.y dist = math.sqrt(dx2 + dy2)
if dist < self.goal_tolerance: self.current_mode = BehaviorMode.IDLE return Twist()
desired = math.atan2(dy, dx) error = desired - self.robot_pose.theta while error > math.pi: error -= 2math.pi while error < -math.pi: error += 2math.pi
cmd = Twist()
cmd.angular.z = max(-self.max_angular_speed,
min( self.max_angular_speed, 2.0 * error))
if abs(error) < 0.3:
cmd.linear.x = min(self.max_linear_speed, 0.5 * dist)
return cmd
6.1.3 ROS 2 Interface
Subscriptions:
/behavior_mode(std_msgs/String) — mode name string/odom(nav_msgs/Odometry) — robot pose for GO_TO_GOAL/scan(sensor_msgs/LaserScan) — laser data for all reactive modes/move_base_simple/goal(geometry_msgs/Point) — navigation goal
Publications:
/cmd_vel_auto(geometry_msgs/Twist) — autonomous velocity commands/robot_status(std_msgs/String) — current mode and state string
The web dashboard (6.2) publishes to /behavior_mode to switch states remotely,
and subscribes to /robot_status to display the active behaviour in the UI.
6.1.4 Mission-Level Execution: Material Handling
The /material-handling page implements a higher-level mission FSM built on
top of the robot’s low-level motion primitives. useMaterialHandling.ts uses
PickAndPlaceTask and TaskQueueManager from the physics library to
model a warehouse pick-and-place mission.
Mission States:
- IDLE — waiting for task queue to populate
- NAVIGATE_TO_PICKUP — drive to material source station
- PICK — align and collect material object
- NAVIGATE_TO_DROPOFF — drive to destination station
- PLACE — align and deposit material object
- RETURN — return to home position or next pickup
// useMaterialHandling.ts (simplified state machine)
const executeMissionStep = async () => {
switch (missionState.value) {
case 'NAVIGATE_TO_PICKUP':
await navigateTo(currentTask.pickupStation)
missionState.value = 'PICK'
break
case 'PICK': await alignAndCollect(currentTask.material) missionState.value = 'NAVIGATE_TO_DROPOFF' break
case 'NAVIGATE_TO_DROPOFF': await navigateTo(currentTask.dropoffStation) missionState.value = 'PLACE' break
case 'PLACE':
await alignAndDeposit(currentTask.material)
metrics.tasksCompleted++
missionState.value = queue.hasNext() ? 'NAVIGATE_TO_PICKUP' : 'IDLE'
break
}
}
TaskQueueManager and Priority Scheduling:
// lib/Robot/PickAndPlaceTask.js
class TaskQueueManager {
addTask(task, priority = 'normal') // 'urgent' | 'normal' | 'low'
getNextTask() // returns highest-priority pending task
markComplete(taskId)
getStats() // { total, completed, pending, failed }
}
HandlingMetrics Interface:
// useMaterialHandling.ts
interface HandlingMetrics {
tasksCompleted: number // successful pick-and-place cycles
totalDistance: number // odometric path length (m)
averageCycleTime: number // mean time per task (s)
efficiency: number // tasks / totalDistance (tasks/m)
collisions: number // obstacle contacts during mission
}
Integration: Theory to Practice
In the integrated system, autonomous_behavior.py provides the autonomous
execution layer that the web dashboard (6.2) switches between via
/behavior_mode topic messages. Voice commands (if implemented) would
resolve to the same mode-switching calls. The Docker deployment (6.4) runs
autonomous_behavior.py as a separate process alongside the virtual robot
node, allowing the behaviour FSM to run independently of the ROS-to-web bridge.
The material handling FSM at the frontend level (useMaterialHandling.ts)
operates in simulation mode without requiring a ROS 2 backend, demonstrating how
complex mission logic can be prototyped entirely in the browser before deployment
to hardware.
Theoretical Design Choices
Why a flat FSM over a behaviour tree? For six states with linear transitions, a flat FSM is easier to debug and audit than a behaviour tree. Each mode’s transition condition is a single conditional check on sensor data, making the control flow entirely predictable. Behaviour trees become advantageous when the number of recovery sequences and conditional branches grows beyond approximately ten nodes.
Why separate /cmd_vel_auto from /cmd_vel? Publishing
autonomous commands to a separate topic allows the web dashboard to arbitrate between
manual and autonomous commands cleanly. The dashboard subscribes to both topics and
applies a priority rule: manual joystick input always overrides /cmd_vel_auto
by publishing directly to /cmd_vel. This avoids race conditions without
requiring a mutex in Python.
Why exploration uses timeout rather than frontier detection? Full frontier-based exploration requires an occupancy grid with consistent coordinate frames. The random-walk with timeout provides functional exploration behaviour without the computational overhead of frontier extraction, making it suitable for educational demonstration on resource-constrained hardware.