feat(craic_localization): add package foundation, configs, and launch files

New ROS2 package for wheel odometry + LiDAR localization. Includes: AMCL config (omni model, likelihood_field), gmapping config (5cm grid, 30 particles), YDLiDAR TminiPro config, launch files for lidar bringup, SLAM mapping, AMCL localization, and sensor calibration, plus RViz config with Map/LaserScan/Odometry displays.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-19 06:30:30 +08:00
parent 46a18f68bf
commit a546aff1a6
13 changed files with 695 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
# P5 AMCL 定位参数 —— 改自 lzu_robot/maps/amcl_config.yaml
# 麦轮底盘用 omni 运动模型;帧与 chassis_odometry / 激光一致。
# 地图文件由 localization.launch.py 的 map:= 参数注入 map_server 的 yaml_filename。
map_server:
ros__parameters:
use_sim_time: false
yaml_filename: "" # 由 launch 的 map:= 覆盖
topic_name: "map"
frame_id: "map"
amcl:
ros__parameters:
use_sim_time: false
# ===== 帧 / 话题 =====
odom_model_type: "omni" # 麦轮全向;差速底盘改 "diff"
global_frame_id: "map"
base_frame_id: "base_footprint"
odom_frame_id: "odom"
scan_topic: "scan"
# ===== 运动模型噪声(按里程计实际精度调)=====
alpha1: 0.005
alpha2: 0.005
alpha3: 0.005
alpha4: 0.005
alpha5: 0.005
# ===== 粒子滤波 =====
min_particles: 500
max_particles: 1000
pf_err: 0.05
resample_interval: 5
# ===== 激光模型(匹配 TminiPro12m=====
laser_model_type: "likelihood_field"
laser_max_range: 12.0
laser_min_range: -1.0
max_beams: 500
sigma_hit: 0.05
z_hit: 0.5
z_rand: 0.03
laser_likelihood_max_dist: 2.0
# ===== 更新策略 =====
update_min_d: 0.05
update_min_a: 0.05
transform_tolerance: 0.5
# ===== 初始位姿(按机器人在场地中的起始点设置;可被 launch 覆盖)=====
set_initial_pose: true
initial_pose:
x: 0.0
y: 0.0
z: 0.0
yaw: 0.0

View File

@@ -0,0 +1,46 @@
# gmapping(slam_gmapping) 建图参数 —— 改自 lzu_robot/src/slam_gmapping/params/slam_gmapping.yaml
# 帧base_frame=base_footprint与 chassis_odometry 一致odom_frame=odommap_frame=map
# 适配 385×385cm 小场地maxUrange 4m 足够覆盖delta 0.05 = 5cm 栅格
# 用 /** 通配键确保参数无论节点全限定名如何都能生效lzu 用 /slam_gmapping 键,
# 其 TF 树恰好有 base_link 才掩盖了键不匹配、参数全默认的问题;我们没有 base_link 会直接报错)
/**:
ros__parameters:
base_frame: base_footprint
odom_frame: odom
map_frame: map
map_update_interval: 5.0
maxUrange: 4.0
maxRange: 6.0
delta: 0.05
particles: 30
linearUpdate: 0.3
angularUpdate: 0.5
temporalUpdate: 1.0
resampleThreshold: 0.5
minimum_score: 0.0
# 运动模型噪声
srr: 0.1
srt: 0.2
str: 0.1
stt: 0.2
# 扫描匹配
sigma: 0.05
kernelSize: 1
lstep: 0.05
astep: 0.05
iterations: 5
lsigma: 0.075
ogain: 3.0
lskip: 0
llsamplerange: 0.01
llsamplestep: 0.01
lasamplerange: 0.005
lasamplestep: 0.005
# 地图范围(米)
xmin: -10.0
xmax: 10.0
ymin: -10.0
ymax: 10.0
occ_thresh: 0.25
transform_publish_period: 0.05
use_sim_time: false

View File

@@ -0,0 +1,24 @@
ydlidar_ros2_driver_node:
ros__parameters:
port: /dev/ttylzulaser
frame_id: laser_frame
ignore_array: ""
baudrate: 230400
lidar_type: 1
device_type: 0
sample_rate: 4
intensity_bit: 8
abnormal_check_count: 4
fixed_resolution: true
reversion: true
inverted: true
auto_reconnect: true
isSingleChannel: false
intensity: true
support_motor_dtr: false
angle_max: 180.0
angle_min: -180.0
range_max: 12.0
range_min: 0.03
frequency: 10.0
invalid_range_is_inf: false

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""定位栈总 bringup轮式里程计 + 激光雷达 + (可选) rviz。
组成完整 TF 树odom ──(chassis_odometry)──> base_footprint ──(静态外参)──> laser_frame
用于 P3 激光外参标定与传感器联调:在 rviz 里同时看 /odom 与 /scan
调 lidar_yaw/lidar_x/lidar_y 直到激光轮廓与实际场地吻合。
用法:
ros2 launch craic_localization bringup.launch.py
ros2 launch craic_localization bringup.launch.py use_rviz:=false
ros2 launch craic_localization bringup.launch.py lidar_yaw:=-1.5707963 intensity:=true
"""
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
pkg_share = get_package_share_directory('craic_localization')
lidar_launch = os.path.join(pkg_share, 'launch', 'lidar.launch.py')
default_rviz = os.path.join(pkg_share, 'rviz', 'localization.rviz')
use_rviz = LaunchConfiguration('use_rviz')
chassis_port = LaunchConfiguration('chassis_port')
rviz_config = LaunchConfiguration('rviz_config')
return LaunchDescription([
DeclareLaunchArgument('use_rviz', default_value='true'),
DeclareLaunchArgument('chassis_port', default_value='/dev/ttylzucar'),
DeclareLaunchArgument('rviz_config', default_value=default_rviz),
# 轮式里程计odom -> base_footprint
Node(
package='craic_localization',
executable='chassis_odometry',
name='chassis_odometry',
output='screen',
parameters=[{'port': chassis_port}],
),
# 激光雷达 + base_footprint -> laser_frame 静态外参(参数见 lidar.launch.py
IncludeLaunchDescription(
PythonLaunchDescriptionSource(lidar_launch),
),
# 可视化(标定 / 联调)
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', rviz_config],
condition=IfCondition(use_rviz),
output='screen',
),
])

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""激光雷达 bringup启动 YDLiDAR TminiPro 驱动 + 发布 base_footprint→laser_frame 静态外参。
YDLiDAR 驱动节点ydlidar_ros2_driver_node内部是普通 rclcpp::Node启动即在 `scan`
话题持续发布 sensor_msgs/LaserScan无需 lifecycle configure/activate 转换。
静态外参为**占位值**(绕 z 轴 -90°对齐 move_try平移取 0需在 P3 按 CRAIC 上
激光雷达的实际安装位置标定。可通过 launch 参数 lidar_x/lidar_y/lidar_z/lidar_yaw 覆盖。
用法:
ros2 launch craic_localization lidar.launch.py
ros2 launch craic_localization lidar.launch.py params_file:=/abs/path/lidar.yaml lidar_yaw:=-1.5707963
"""
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.descriptions import ParameterValue
def generate_launch_description():
pkg_share = get_package_share_directory('craic_localization')
default_params = os.path.join(pkg_share, 'config', 'lidar.yaml')
params_file = LaunchConfiguration('params_file')
lidar_x = LaunchConfiguration('lidar_x')
lidar_y = LaunchConfiguration('lidar_y')
lidar_z = LaunchConfiguration('lidar_z')
lidar_yaw = LaunchConfiguration('lidar_yaw')
base_frame = LaunchConfiguration('base_frame')
laser_frame = LaunchConfiguration('laser_frame')
# 决定数据包解析格式的关键参数,作为 launch 覆盖项暴露,便于排查 Check Sum 报错:
# 基础 Tmini 无强度 -> intensity:=falseTminiPro 有强度 -> intensity:=true
intensity = LaunchConfiguration('intensity')
sample_rate = LaunchConfiguration('sample_rate')
baudrate = LaunchConfiguration('baudrate')
return LaunchDescription([
DeclareLaunchArgument('params_file', default_value=default_params,
description='YDLiDAR 参数文件路径'),
DeclareLaunchArgument('base_frame', default_value='base_footprint'),
DeclareLaunchArgument('laser_frame', default_value='laser_frame'),
# 占位外参P3 标定后覆盖
DeclareLaunchArgument('lidar_x', default_value='0.0'),
DeclareLaunchArgument('lidar_y', default_value='0.0'),
DeclareLaunchArgument('lidar_z', default_value='0.0'),
DeclareLaunchArgument('lidar_yaw', default_value='-3.14159',
description='激光相对底盘偏航角(rad)。-180°=让机体X轴对准底盘实际前进(W键)方向;'
'若实测前后颠倒,改 0.0'),
# 数据包格式覆盖项(排查 Check Sum 报错用)
DeclareLaunchArgument('intensity', default_value='true',
description='设备是否输出强度:基础 Tmini=falseTminiPro=true。不匹配会刷 Check Sum 报错'),
DeclareLaunchArgument('sample_rate', default_value='4',
description='采样率(K)Tmini 系列为 4'),
DeclareLaunchArgument('baudrate', default_value='230400'),
Node(
package='ydlidar_ros2_driver',
executable='ydlidar_ros2_driver_node',
name='ydlidar_ros2_driver_node',
output='screen',
emulate_tty=True,
parameters=[
params_file,
{
'intensity': ParameterValue(intensity, value_type=bool),
'sample_rate': ParameterValue(sample_rate, value_type=int),
'baudrate': ParameterValue(baudrate, value_type=int),
},
],
namespace='/',
),
Node(
package='tf2_ros',
executable='static_transform_publisher',
name='static_tf_base_to_laser',
arguments=[
'--x', lidar_x, '--y', lidar_y, '--z', lidar_z,
'--yaw', lidar_yaw, '--pitch', '0.0', '--roll', '0.0',
'--frame-id', base_frame, '--child-frame-id', laser_frame,
],
),
# base_footprint -> base_link 恒等变换。
# slam_gmapping(本 build)不从参数读 base_frame硬编码默认 base_link
# 提供该帧让其默认值可解析。本工程 base_footprint 即底盘基准,两者重合。
Node(
package='tf2_ros',
executable='static_transform_publisher',
name='static_tf_base_link',
arguments=[
'--x', '0.0', '--y', '0.0', '--z', '0.0',
'--yaw', '0.0', '--pitch', '0.0', '--roll', '0.0',
'--frame-id', base_frame, '--child-frame-id', 'base_link',
],
),
])

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""P5 精准定位:激光 + rf2o 激光里程计 + 已知地图 + AMCL。
完整 TF 树map ─(amcl)→ odom ─(rf2o)→ base_footprint ─(静态)→ laser_frame
odom→base_footprint 由 rf2o 从 /scan 连续提供(不依赖底盘里程计板)AMCL 用它做运动模型、
激光匹配已知地图做校正,输出绝对、无漂移位姿。需要 P4 建好并存盘的地图。
说明:底盘轮式里程计板上电后仅短时上报,暂不参与 odom→base待其连续上报问题解决后
可用 robot_localization EKF 融合轮速 + rf2o 再喂 AMCL 提升鲁棒性。
用法:
ros2 launch craic_localization localization.launch.py map:=/abs/path/maps/craic.yaml
# 起点不在地图原点时rviz 用 "2D Pose Estimate" 给初值,或:
ros2 launch craic_localization localization.launch.py map:=... init_x:=0.3 init_y:=0.2 init_yaw:=1.57
"""
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.descriptions import ParameterValue
def generate_launch_description():
pkg_share = get_package_share_directory('craic_localization')
lidar_launch = os.path.join(pkg_share, 'launch', 'lidar.launch.py')
amcl_params = os.path.join(pkg_share, 'config', 'amcl.yaml')
default_rviz = os.path.join(pkg_share, 'rviz', 'localization.rviz')
default_map = os.path.join(pkg_share, 'maps', 'craic.yaml')
use_rviz = LaunchConfiguration('use_rviz')
map_yaml = LaunchConfiguration('map')
init_x = LaunchConfiguration('init_x')
init_y = LaunchConfiguration('init_y')
init_yaw = LaunchConfiguration('init_yaw')
show_points = LaunchConfiguration('show_points')
points_file = LaunchConfiguration('points_file')
return LaunchDescription([
DeclareLaunchArgument('use_rviz', default_value='true'),
DeclareLaunchArgument('map', default_value=default_map,
description='地图 yaml 路径(P4 由 map_saver 存盘得到)'),
DeclareLaunchArgument('init_x', default_value='0.0'),
DeclareLaunchArgument('init_y', default_value='0.0'),
DeclareLaunchArgument('init_yaw', default_value='0.0'),
DeclareLaunchArgument('show_points', default_value='true'),
DeclareLaunchArgument('points_file',
default_value=os.path.join(pkg_share, 'config', 'taught_points.yaml'),
description='示教点位文件(rviz 显示);想实时看示教就指向你的 taught_points.yaml'),
# ===== 传感器 + 激光里程计(odom -> base_footprint) =====
IncludeLaunchDescription(PythonLaunchDescriptionSource(lidar_launch)),
Node(
package='rf2o_laser_odometry',
executable='rf2o_laser_odometry_node',
name='rf2o_laser_odometry',
output='screen',
parameters=[{
'laser_scan_topic': '/scan',
'odom_topic': '/odom',
'publish_tf': True,
'base_frame_id': 'base_footprint',
'odom_frame_id': 'odom',
'init_pose_from_topic': '',
'freq': 10.0,
}],
),
# ===== 地图服务 =====
Node(
package='nav2_map_server',
executable='map_server',
name='map_server',
output='screen',
parameters=[amcl_params, {'yaml_filename': map_yaml}],
),
# ===== AMCL 定位 =====
Node(
package='nav2_amcl',
executable='amcl',
name='amcl',
output='screen',
parameters=[amcl_params, {
'set_initial_pose': True,
'initial_pose.x': ParameterValue(init_x, value_type=float),
'initial_pose.y': ParameterValue(init_y, value_type=float),
'initial_pose.yaw': ParameterValue(init_yaw, value_type=float),
}],
),
# ===== 生命周期管理:自动 configure+activate map_server 与 amcl =====
Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager_localization',
output='screen',
parameters=[{
'use_sim_time': False,
'autostart': True,
'node_names': ['map_server', 'amcl'],
}],
),
# ===== 示教点位可视化 =====
Node(
package='craic_localization',
executable='show_points',
name='show_points',
output='screen',
parameters=[{'points_file': points_file}],
condition=IfCondition(show_points),
),
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', default_rviz],
condition=IfCondition(use_rviz),
output='screen',
),
])

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""P4 建图:激光 + rf2o 激光里程计 + slam_gmapping(SLAM)。
odom→base_footprint 由 rf2o(从连续 /scan 推算)提供,**不依赖底盘里程计板** ——
该板上电后仅短时上报,会让 odom TF 冻结、gmapping 报 "queue is full" 丢弃所有 scan。
本管线对齐 lzu_robot 可正常建图的做法rf2o 提供 odom→basegmapping 出图)。
用法:
ros2 launch craic_localization mapping.launch.py
# 另开终端遥控走图;满意后存盘:
ros2 run nav2_map_server map_saver_cli -f <pkg>/maps/craic --ros-args -p save_map_timeout:=10000.0
"""
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
pkg_share = get_package_share_directory('craic_localization')
lidar_launch = os.path.join(pkg_share, 'launch', 'lidar.launch.py')
gmapping_params = os.path.join(pkg_share, 'config', 'gmapping.yaml')
default_rviz = os.path.join(pkg_share, 'rviz', 'localization.rviz')
use_rviz = LaunchConfiguration('use_rviz')
return LaunchDescription([
DeclareLaunchArgument('use_rviz', default_value='true'),
# 激光 + 静态外参(base_footprint->laser_frame, base_footprint->base_link)
IncludeLaunchDescription(PythonLaunchDescriptionSource(lidar_launch)),
# 激光里程计:从 /scan 连续输出 odom -> base_footprint建图运动来源
Node(
package='rf2o_laser_odometry',
executable='rf2o_laser_odometry_node',
name='rf2o_laser_odometry',
output='screen',
parameters=[{
'laser_scan_topic': '/scan',
'odom_topic': '/odom',
'publish_tf': True,
'base_frame_id': 'base_footprint',
'odom_frame_id': 'odom',
'init_pose_from_topic': '',
'freq': 10.0,
}],
),
# SLAM发布 /map 与 TF map -> odom
Node(
package='slam_gmapping',
executable='slam_gmapping',
name='slam_gmapping',
output='screen',
parameters=[gmapping_params],
),
Node(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', default_rviz],
condition=IfCondition(use_rviz),
output='screen',
),
])

View File

@@ -0,0 +1,26 @@
<?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>craic_localization</name>
<version>0.0.0</version>
<description>Wheel odometry + LiDAR localization for the CRAIC mobile base</description>
<maintainer email="fallensigh@gmail.com">fallensigh</maintainer>
<license>MIT</license>
<depend>rclpy</depend>
<depend>nav_msgs</depend>
<depend>geometry_msgs</depend>
<depend>std_msgs</depend>
<depend>tf2_ros</depend>
<depend>visualization_msgs</depend>
<exec_depend>python3-yaml</exec_depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>

View File

@@ -0,0 +1,130 @@
Panels:
- Class: rviz_common/Displays
Name: Displays
Property Tree Widget:
Expanded:
- /TF1
- /LaserScan1
- /LaserScan1/Topic1
- /Odometry1
Splitter Ratio: 0.5
- Class: rviz_common/Views
Name: Views
Visualization Manager:
Class: ""
Displays:
- Class: rviz_default_plugins/Grid
Name: Grid
Enabled: true
Reference Frame: <Fixed Frame>
Plane Cell Count: 20
Cell Size: 0.5
Color: 160; 160; 164
Line Style:
Line Width: 0.03
Value: Lines
Value: true
- Class: rviz_default_plugins/TF
Name: TF
Enabled: true
Show Names: true
Show Axes: true
Show Arrows: true
Marker Scale: 0.5
Update Interval: 0
Frame Timeout: 15
Value: true
- Class: rviz_default_plugins/Map
Name: Map
Enabled: true
Topic:
Value: /map
Depth: 1
Durability Policy: Transient Local
History Policy: Keep Last
Reliability Policy: Reliable
Color Scheme: map
Draw Behind: true
Alpha: 0.7
Value: true
- Class: rviz_default_plugins/LaserScan
Name: LaserScan
Enabled: true
Topic:
Value: /scan
Depth: 5
Durability Policy: Volatile
History Policy: Keep Last
Reliability Policy: Best Effort
Filter size: 10
Style: Points
Size (m): 0.02
Size (Pixels): 3
Color Transformer: FlatColor
Color: 255; 30; 30
Decay Time: 0
Position Transformer: XYZ
Value: true
- Class: rviz_default_plugins/Odometry
Name: Odometry
Enabled: true
Topic:
Value: /odom
Depth: 10
Durability Policy: Volatile
History Policy: Keep Last
Reliability Policy: Reliable
Keep: 1
Position Tolerance: 0.05
Angle Tolerance: 0.05
Shape:
Value: Arrow
Color: 30; 255; 30
Shaft Length: 0.3
Shaft Radius: 0.03
Head Length: 0.1
Head Radius: 0.06
Alpha: 1
Covariance:
Value: false
Value: true
- Class: rviz_default_plugins/MarkerArray
Name: TaughtPoints
Enabled: true
Topic:
Value: /taught_points
Depth: 5
Durability Policy: Volatile
History Policy: Keep Last
Reliability Policy: Reliable
Namespaces: {}
Value: true
Global Options:
Fixed Frame: odom
Background Color: 48; 48; 48
Frame Rate: 30
Tools:
- Class: rviz_default_plugins/MoveCamera
- Class: rviz_default_plugins/Select
- Class: rviz_default_plugins/FocusCamera
- Class: rviz_default_plugins/Measure
Views:
Current:
Class: rviz_default_plugins/Orbit
Name: Current View
Distance: 8
Focal Point:
X: 0
Y: 0
Z: 0
Pitch: 1.2
Yaw: 3.14
Target Frame: <Fixed Frame>
Saved: ~
Window Geometry:
Height: 800
Width: 1200
Displays:
collapsed: false
Views:
collapsed: false

View File

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

View File

@@ -0,0 +1,46 @@
import os
from glob import glob
from setuptools import find_packages, setup
package_name = 'craic_localization'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'config'),
glob('config/*.yaml')),
(os.path.join('share', package_name, 'launch'),
glob('launch/*.launch.py')),
(os.path.join('share', package_name, 'rviz'),
glob('rviz/*.rviz')),
(os.path.join('share', package_name, 'urdf'),
glob('urdf/*')),
(os.path.join('share', package_name, 'maps'),
glob('maps/*')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='fallensigh',
maintainer_email='fallensigh@gmail.com',
description='Wheel odometry + LiDAR localization for the CRAIC mobile base',
license='MIT',
extras_require={
'test': [
'pytest',
],
},
entry_points={
'console_scripts': [
'chassis_odometry = craic_localization.chassis_odometry:main',
'teach_points = craic_localization.teach_points:main',
'navigate_to_point = craic_localization.navigate_to_point:main',
'show_points = craic_localization.show_points:main',
],
},
)