Initial commit

This commit is contained in:
1 2026-08-13 14:35:17 +08:00
commit a4e596c3aa
234 changed files with 81101 additions and 0 deletions

View file

@ -0,0 +1,17 @@
mission_manager:
ros__parameters:
autostart: false
auto_arm_complete_timeout_sec: 8.0
platform_side: "left"
blue_zone_label: "blue"
green_zone_label: "green"
approach_speed_mps: 0.18
slope_speed_mps: 0.32
lateral_speed_mps: 0.12
turn_speed_radps: 0.65
yaw_tolerance_deg: 5.0
ultrasonic_stop_distance_m: 0.35
signboard_stop_distance_m: 0.70
board_hold_sec: 3.0
alignment_tolerance: 0.12
max_wait_signal_board_sec: 30.0

View file

@ -0,0 +1,21 @@
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
config = os.path.join(
get_package_share_directory('mission_manager'),
'config',
'mission_manager.yaml'
)
return LaunchDescription([
Node(
package='mission_manager',
executable='mission_manager',
name='mission_manager',
output='screen',
parameters=[config],
)
])

View file

@ -0,0 +1,310 @@
import math
from enum import Enum, auto
from typing import Optional
import rclpy
from geometry_msgs.msg import PointStamped, Twist
from nav_msgs.msg import Odometry
from rclpy.duration import Duration
from rclpy.node import Node
from sensor_msgs.msg import Imu, Range
from std_msgs.msg import Bool, String
class MissionState(Enum):
IDLE = auto()
APPROACH_SLOPE_ENTRY = auto()
PICK_AND_PLACE_SPONGE = auto()
TURN_LEFT_90 = auto()
RUSH_SLOPE = auto()
CHECK_SIGNAL_BOARD = auto()
MOVE_TO_BLOCK_1 = auto()
PICK_BLOCK_1 = auto()
PLACE_BLOCK_1 = auto()
MOVE_TO_BLOCK_2 = auto()
PICK_BLOCK_2 = auto()
PLACE_BLOCK_2 = auto()
FINISHED = auto()
class MissionManager(Node):
def __init__(self) -> None:
super().__init__('mission_manager')
self.declare_parameter('autostart', False)
self.declare_parameter('auto_arm_complete_timeout_sec', 2.0)
self.declare_parameter('platform_side', 'left')
self.declare_parameter('blue_zone_label', 'blue')
self.declare_parameter('green_zone_label', 'green')
self.declare_parameter('approach_speed_mps', 0.18)
self.declare_parameter('slope_speed_mps', 0.32)
self.declare_parameter('lateral_speed_mps', 0.12)
self.declare_parameter('turn_speed_radps', 0.65)
self.declare_parameter('yaw_tolerance_deg', 5.0)
self.declare_parameter('ultrasonic_stop_distance_m', 0.35)
self.declare_parameter('signboard_stop_distance_m', 0.70)
self.declare_parameter('board_hold_sec', 3.0)
self.declare_parameter('alignment_tolerance', 0.12)
self.declare_parameter('max_wait_signal_board_sec', 30.0)
self.autostart = self.get_parameter('autostart').value
self.auto_arm_complete_timeout_sec = float(self.get_parameter('auto_arm_complete_timeout_sec').value)
self.platform_side = str(self.get_parameter('platform_side').value)
self.blue_zone_label = str(self.get_parameter('blue_zone_label').value)
self.green_zone_label = str(self.get_parameter('green_zone_label').value)
self.approach_speed_mps = float(self.get_parameter('approach_speed_mps').value)
self.slope_speed_mps = float(self.get_parameter('slope_speed_mps').value)
self.lateral_speed_mps = float(self.get_parameter('lateral_speed_mps').value)
self.turn_speed_radps = float(self.get_parameter('turn_speed_radps').value)
self.yaw_tolerance_rad = math.radians(float(self.get_parameter('yaw_tolerance_deg').value))
self.ultrasonic_stop_distance_m = float(self.get_parameter('ultrasonic_stop_distance_m').value)
self.signboard_stop_distance_m = float(self.get_parameter('signboard_stop_distance_m').value)
self.board_hold_sec = float(self.get_parameter('board_hold_sec').value)
self.alignment_tolerance = float(self.get_parameter('alignment_tolerance').value)
self.max_wait_signal_board_sec = float(self.get_parameter('max_wait_signal_board_sec').value)
self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)
self.arm_cmd_pub = self.create_publisher(String, '/arm_command', 10)
self.state_pub = self.create_publisher(String, '/mission_manager/state', 10)
self.create_subscription(Range, '/ultrasonic/front/range', self.on_range, 10)
self.create_subscription(Imu, '/imu', self.on_imu, 10)
self.create_subscription(Odometry, '/odom', self.on_odom, 10)
self.create_subscription(String, '/signal_board/result', self.on_signal_board, 10)
self.create_subscription(PointStamped, '/depth_target/point', self.on_depth_target, 10)
self.create_subscription(String, '/arm/status', self.on_arm_status, 10)
self.create_subscription(Bool, '/mission/start', self.on_start, 10)
self.timer = self.create_timer(0.05, self.on_timer)
self.state = MissionState.APPROACH_SLOPE_ENTRY if self.autostart else MissionState.IDLE
self.current_range_m: Optional[float] = None
self.current_yaw: Optional[float] = None
self.current_pose = None
self.signal_board_result = 'unknown'
self.depth_target: Optional[PointStamped] = None
self.arm_busy = False
self.pending_arm_done_deadline = None
self.turn_start_yaw: Optional[float] = None
self.state_enter_time = self.get_clock().now()
self.block_pick_count = 0
self.sponge_done = False
self.signal_idle_seen = False
self.publish_state('READY' if self.autostart else 'IDLE_WAIT_START')
def on_range(self, msg: Range) -> None:
self.current_range_m = float(msg.range)
def on_imu(self, msg: Imu) -> None:
q = msg.orientation
siny_cosp = 2.0 * (q.w * q.z + q.x * q.y)
cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
self.current_yaw = math.atan2(siny_cosp, cosy_cosp)
def on_odom(self, msg: Odometry) -> None:
self.current_pose = msg.pose.pose
def on_signal_board(self, msg: String) -> None:
self.signal_board_result = msg.data.strip()
def on_depth_target(self, msg: PointStamped) -> None:
self.depth_target = msg
def on_arm_status(self, msg: String) -> None:
data = msg.data.strip().lower()
if data in {'busy', 'running'}:
self.arm_busy = True
elif data in {'done', 'idle', 'ok', 'success'}:
self.arm_busy = False
self.pending_arm_done_deadline = None
elif data in {'failed', 'error'}:
self.arm_busy = False
self.pending_arm_done_deadline = None
self.get_logger().warning('Arm reported failure, mission pauses at current state.')
def on_start(self, msg: Bool) -> None:
if msg.data and self.state == MissionState.IDLE:
self.transition_to(MissionState.APPROACH_SLOPE_ENTRY, 'external start signal')
elif (not msg.data) and self.state != MissionState.IDLE:
self.stop_robot()
self.transition_to(MissionState.IDLE, 'external stop signal')
def publish_state(self, detail: str) -> None:
msg = String()
msg.data = f'{self.state.name}:{detail}'
self.state_pub.publish(msg)
def transition_to(self, new_state: MissionState, reason: str) -> None:
self.stop_robot()
self.state = new_state
self.state_enter_time = self.get_clock().now()
if new_state != MissionState.TURN_LEFT_90:
self.turn_start_yaw = None
self.publish_state(reason)
self.get_logger().info(f'Transition -> {new_state.name}: {reason}')
def state_elapsed(self) -> float:
return (self.get_clock().now() - self.state_enter_time).nanoseconds / 1e9
def stop_robot(self) -> None:
self.cmd_pub.publish(Twist())
def send_cmd(self, linear_x: float = 0.0, linear_y: float = 0.0, angular_z: float = 0.0) -> None:
cmd = Twist()
cmd.linear.x = float(linear_x)
cmd.linear.y = float(linear_y)
cmd.angular.z = float(angular_z)
self.cmd_pub.publish(cmd)
def send_arm_command(self, command: str) -> None:
msg = String()
msg.data = command
self.arm_cmd_pub.publish(msg)
self.arm_busy = True
self.pending_arm_done_deadline = self.get_clock().now() + Duration(seconds=self.auto_arm_complete_timeout_sec)
self.get_logger().info(f'Arm command -> {command}')
def arm_action_finished(self) -> bool:
if not self.arm_busy:
return True
if self.pending_arm_done_deadline is not None and self.get_clock().now() >= self.pending_arm_done_deadline:
self.arm_busy = False
self.pending_arm_done_deadline = None
self.get_logger().warning('Arm timeout auto-completed for integration testing.')
return True
return False
@staticmethod
def angle_wrap(angle: float) -> float:
while angle > math.pi:
angle -= 2.0 * math.pi
while angle < -math.pi:
angle += 2.0 * math.pi
return angle
def depth_target_aligned(self) -> bool:
if self.depth_target is None:
return False
return abs(self.depth_target.point.y) <= self.alignment_tolerance
def on_timer(self) -> None:
if self.state == MissionState.IDLE:
self.stop_robot()
return
if self.state == MissionState.APPROACH_SLOPE_ENTRY:
if (not self.sponge_done) and self.depth_target is not None and self.depth_target.point.x <= 0.55:
self.stop_robot()
self.send_arm_command(f'pick_and_place_platform:{self.platform_side}')
self.sponge_done = True
self.transition_to(MissionState.PICK_AND_PLACE_SPONGE, 'depth target in pickup window')
return
if self.current_range_m is not None and self.current_range_m <= self.ultrasonic_stop_distance_m:
self.transition_to(MissionState.TURN_LEFT_90, 'ultrasonic reached slope-entry stop line')
return
self.send_cmd(linear_x=self.approach_speed_mps)
return
if self.state == MissionState.PICK_AND_PLACE_SPONGE:
if self.arm_action_finished():
self.transition_to(MissionState.APPROACH_SLOPE_ENTRY, 'sponge placed on side platform')
return
if self.state == MissionState.TURN_LEFT_90:
if self.current_yaw is None:
self.stop_robot()
return
if self.turn_start_yaw is None:
self.turn_start_yaw = self.current_yaw
delta = self.angle_wrap(self.current_yaw - self.turn_start_yaw)
target = math.pi / 2.0
if abs(target - abs(delta)) <= self.yaw_tolerance_rad:
self.transition_to(MissionState.RUSH_SLOPE, 'left turn completed')
return
self.send_cmd(angular_z=self.turn_speed_radps)
return
if self.state == MissionState.RUSH_SLOPE:
if self.current_range_m is not None and self.current_range_m <= self.signboard_stop_distance_m:
self.transition_to(MissionState.CHECK_SIGNAL_BOARD, 'arrived at signboard check point')
return
self.send_cmd(linear_x=self.slope_speed_mps)
return
if self.state == MissionState.CHECK_SIGNAL_BOARD:
self.stop_robot()
if self.signal_board_result.startswith('idle'):
if not self.signal_idle_seen:
self.signal_idle_seen = True
self.state_enter_time = self.get_clock().now()
self.publish_state(f'signal board idle confirmed: {self.signal_board_result}')
elif self.state_elapsed() >= self.board_hold_sec:
self.transition_to(MissionState.MOVE_TO_BLOCK_1, 'signal board display hold completed')
return
if self.signal_board_result == 'busy' and self.state_elapsed() >= self.max_wait_signal_board_sec:
self.get_logger().warning('Signal board stayed busy too long, mission still waits for manual intervention.')
self.state_enter_time = self.get_clock().now()
return
if self.state == MissionState.MOVE_TO_BLOCK_1:
if self.depth_target is not None and self.depth_target_aligned() and self.depth_target.point.x <= 0.45:
self.send_arm_command('pick_block:1')
self.transition_to(MissionState.PICK_BLOCK_1, 'first floor block aligned')
return
self.send_cmd(linear_y=self.lateral_speed_mps)
return
if self.state == MissionState.PICK_BLOCK_1:
if self.arm_action_finished():
self.send_arm_command(f'place_block:{self.blue_zone_label}')
self.transition_to(MissionState.PLACE_BLOCK_1, 'first pick completed')
return
if self.state == MissionState.PLACE_BLOCK_1:
if self.arm_action_finished():
self.block_pick_count = 1
self.transition_to(MissionState.MOVE_TO_BLOCK_2, 'first block placed to blue zone')
return
if self.state == MissionState.MOVE_TO_BLOCK_2:
if self.depth_target is not None and self.depth_target_aligned() and self.depth_target.point.x <= 0.45:
self.send_arm_command('pick_block:2')
self.transition_to(MissionState.PICK_BLOCK_2, 'second floor block aligned')
return
self.send_cmd(linear_y=self.lateral_speed_mps)
return
if self.state == MissionState.PICK_BLOCK_2:
if self.arm_action_finished():
self.send_arm_command(f'place_block:{self.green_zone_label}')
self.transition_to(MissionState.PLACE_BLOCK_2, 'second pick completed')
return
if self.state == MissionState.PLACE_BLOCK_2:
if self.arm_action_finished():
self.block_pick_count = 2
self.transition_to(MissionState.FINISHED, 'all competition actions completed')
return
if self.state == MissionState.FINISHED:
self.stop_robot()
return
def main() -> None:
rclpy.init()
node = MissionManager()
try:
rclpy.spin(node)
finally:
node.stop_robot()
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>mission_manager</name>
<version>0.1.0</version>
<description>Mission state machine for the RDK X5 logistics competition robot.</description>
<maintainer email="openai@example.com">OpenAI</maintainer>
<license>Apache-2.0</license>
<depend>rclpy</depend>
<depend>geometry_msgs</depend>
<depend>nav_msgs</depend>
<depend>sensor_msgs</depend>
<depend>std_msgs</depend>
<exec_depend>ros2launch</exec_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>

View file

@ -0,0 +1,4 @@
[develop]
script_dir=$base/lib/mission_manager
[install]
install_scripts=$base/lib/mission_manager

View file

@ -0,0 +1,26 @@
from setuptools import setup
package_name = 'mission_manager'
setup(
name=package_name,
version='0.1.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch', ['launch/mission_manager.launch.py']),
('share/' + package_name + '/config', ['config/mission_manager.yaml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='OpenAI',
maintainer_email='openai@example.com',
description='Mission state machine for RDK X5 logistics robot',
license='Apache-2.0',
entry_points={
'console_scripts': [
'mission_manager = mission_manager.mission_manager_node:main',
],
},
)