Before working with this repository, ensure that:
- ROS2 Jazzy is installed
- colcon is installed
- You are using a Linux environment, for example Ubuntu inside UTM
This README assumes ROS2 is installed under:
/opt/ros/jazzy
ROS2 is not active automatically when a terminal is opened. Before using colcon or ros2 commands, run:
source /opt/ros/jazzy/setup.bashThis must be done in every new terminal unless you make it automatic.
If you want ROS2 to always be available:
echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc
source ~/.bashrcYou still need to source your workspace separately after building it.
All ROS2 development must happen inside a workspace that contains:
srcfor packagesbuildcreated by colconinstallcreated by colconlogcreated by colcon
Your repositories must go inside src.
mkdir -p ~/mapping_ws/src
cd ~/mapping_ws/srcFrom inside ~/mapping_ws/src:
cd ~/mapping_ws/src
# Mapping and localization repo
git clone git@github.com:WE-Autopilot/mapping-and-localization.git
# Shared messages repo used across AP1
git clone git@github.com:WE-Autopilot/ap1_msgs.gitResulting structure:
mapping_ws
src
mapping_and_localization
mapping_and_localization_cpp
mapping_localization_python
ap1_msgs
All shared custom messages for AP1 should live in ap1_msgs.
If you need a new message type, add it to ap1_msgs rather than creating a new message package inside this repository.
Running:
colcon buildmakes colcon:
- Compile C plus plus code
- Install binaries into
install - Register Python entry points
- Generate environment setup scripts
- Enable
ros2 runto find the nodes
You must build when:
- C plus plus code changes
package.xmlchangesCMakeLists.txtchangessetup.cfgentry points change
cd ~/mapping_ws
source /opt/ros/jazzy/setup.bash
colcon build --symlink-installSymlink mode is recommended for development.
To use your nodes:
source ~/mapping_ws/install/setup.bashYou must source the workspace:
- In new terminals
- After every
colcon build - Before running
ros2commands
Python code works as long as you have sourced the workspace once in that terminal. C plus plus requires sourcing after each rebuild.
If you use this workspace every day:
echo "source ~/mapping_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrcMake sure the order in your ~/.bashrc is:
source /opt/ros/jazzy/setup.bash
source ~/mapping_ws/install/setup.bashmapping_and_localization
mapping_and_localization_cpp
CMakeLists.txt
package.xml
src
<cpp_nodes>.cpp
mapping_localization_python
package.xml
setup.cfg
setup.py
mapping_localization_python
<python_nodes>.py
.gitignore
README.md
Only the two folders with package.xml are ROS2 packages inside this repository.
The ap1_msgs package is a separate repository at:
mapping_ws
src
ap1_msgs
It contains shared message definitions that can be used by any AP1 package. Add new messages there when needed.
In CMakeLists.txt:
add_executable(localization_node src/localization_node.cpp)
ament_target_dependencies(localization_node
rclcpp
)
install(TARGETS
localization_node
DESTINATION lib/${PROJECT_NAME}
)In package.xml:
<depend>rclcpp</depend>
<depend>std_msgs</depend>If your C plus plus node uses messages from ap1_msgs (e.g., #include "ap1_msgs/msg/..."), add:
<depend>ap1_msgs</depend>In CMakeLists.txt:
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(ap1_msgs REQUIRED) # Only if you use ap1_msgs
ament_target_dependencies(localization_node
rclcpp
std_msgs
ap1_msgs # Only if you use ap1_msgs
)In setup.cfg:
[options.entry_points]
console_scripts =
pose_node = mapping_localization_python.pose_node:mainIn package.xml:
<depend>rclpy</depend>
<depend>std_msgs</depend>If your Python node uses messages from ap1_msgs, also add:
<depend>ap1_msgs</depend>Python files live inside:
mapping_localization_python/mapping_localization_python/
You must source the workspace first:
source ~/mapping_ws/install/setup.bashRun a C plus plus node:
ros2 run mapping_and_localization_cpp localization_nodeRun a Python node:
ros2 run mapping_localization_python pose_node| What changed | Need colcon build --symlink-install |
Reason |
|---|---|---|
| Python file (.py) | No | Symlink points to source file |
| Launch, YAML, configuration | No | Symlinks update instantly |
| C plus plus file (.cpp, .hpp) | Yes | Must recompile |
package.xml |
Yes | Metadata changed |
CMakeLists.txt |
Yes | Build system instructions changed |
New Python node in setup.cfg |
Yes | Must regenerate entry point |
Editing setup.cfg text only |
Usually no | Not used at runtime |
| Adding a new package | Yes | Build system must detect it |
- Edit the Python file
- Source the workspace once in any terminal
- Run the node
No rebuild required unless adding new entry points.
-
Edit the C plus plus file
-
Run:
colcon build --symlink-install
-
Source again:
source install/setup.bash -
Run the node
- Rebuild with symlink
- Source workspace
- Run the node
- Only folders with
package.xmlare ROS2 packages build,install,logand.vscodeshould not be committed- Always run
colcon buildfrom the workspace root, never inside a package - Never run CMake manually
- Shared messages should live in ap1_msgs. If you need a new message type for mapping or localization, add it there and then depend on
ap1_msgsin your package.
This repository now includes a minimum mapping/localization pipeline in the mapping_localization_python package.
It is designed to:
- Consume perception entities and lane boundaries
- Publish immediately on each incoming perception callback
- Fill missing lane boundaries by holding the last valid left/right boundary for a short timeout
- Generate deterministic AP1 sample perception data when no Perception sample exists yet
- Optionally launch Kitware
lidar_slam_nodeand bridge/slam_odomto AP1 localization topics - Publish an accumulated distance estimate from SLAM odometry
- Merge Kitware keypoint maps into
/slam_mapand expose/save_map - Expose a point-registry service for planning and mapping
Inputs:
/ap1/perception/entities(ap1_msgs/msg/EntityStateArray)/ap1/perception/lanes(ap1_msgs/msg/LaneBoundaries)
Outputs:
/ap1/mapping/entities(ap1_msgs/msg/EntityStateArray)/ap1/mapping/lanes(ap1_msgs/msg/LaneBoundaries)
Key params:
lane_timeout_sec(default1.0)min_lane_points(default2)
Input:
/slam_odom(nav_msgs/msg/Odometry)
Outputs:
/ap1/localization/odom(nav_msgs/msg/Odometry)/ap1/localization/pose(geometry_msgs/msg/PoseStamped)/ap1/localization/slam_pose(geometry_msgs/msg/PoseWithCovarianceStamped, framemap)/ap1/localization/distance(ap1_msgs/msg/FloatStamped, meters travelled)
Key params:
publish_pose(defaulttrue)publish_slam_pose(defaulttrue)publish_distance(defaulttrue)slam_pose_frame(defaultmap)slam_pose_publish_rate_hz(default20.0, clamped to minimum10.0)slam_pose_frequency_log_interval_sec(default5.0)distance_jump_threshold_m(default10.0)
Inputs:
/maps/edges(sensor_msgs/msg/PointCloud2)/maps/intensity_edges(sensor_msgs/msg/PointCloud2)/maps/planes(sensor_msgs/msg/PointCloud2)/maps/blobs(sensor_msgs/msg/PointCloud2)
Outputs:
/slam_map(sensor_msgs/msg/PointCloud2, framemap)/slam_map_grid(nav_msgs/msg/OccupancyGrid, optional)
Services:
/save_map(std_srvs/srv/Empty)
Behavior:
- Merges the latest Kitware keypoint map topics into a single point cloud
- Publishes the merged map at a configurable low rate (default
1.0 Hz) - Saves a merged ASCII PCD and, when Kitware is available, triggers
lidar_slam/save_pcso the same prefix can be reused viamaps.initial_maps
Key params:
map_publish_rate_hz(default1.0)map_frame(defaultmap)publish_occupancy_grid(defaultfalse)save_map_prefix(default~/slam_maps/slam_map)
Outputs:
/ap1/perception/entities(ap1_msgs/msg/EntityStateArray)/ap1/perception/lanes(ap1_msgs/msg/LaneBoundaries)
Behavior:
- Publishes both left and right lane boundaries on every cycle
- Uses
16waypoints per lane by default - Adds larger deterministic noise to the tail of each lane because Perception only specified that predictions become unstable near the end
Key params:
publish_rate_hz(default10.0)waypoint_count(default16)waypoint_spacing_m(default0.5, soft target of2waypoints per meter)lane_width_m(default3.5)tail_instability_points(default4)tail_noise_max_m(default0.9)
Services:
/ap1/mapping/point_registry/create(ap1_msgs/srv/CreateStoredPoint)/ap1/mapping/point_registry/delete(ap1_msgs/srv/DeleteStoredPoint)/ap1/mapping/point_registry/lookup(ap1_msgs/srv/LookupStoredPoint)
Behavior:
- Create stores a
geometry_msgs/Pointand returns a generated ID - Delete removes a stored point by ID
- Lookup returns a stored point by ID when present
Launch everything:
ros2 launch mapping_localization_python mapping_pipeline.launch.pyEnable Kitware SLAM in the same launch:
ros2 launch mapping_localization_python mapping_pipeline.launch.py use_kitware_slam:=trueEnable synthetic AP1 perception input for pipeline smoke tests:
ros2 launch mapping_localization_python mapping_pipeline.launch.py \
use_synthetic_perception:=trueRun synthetic perception and Kitware SLAM together, including /slam_map:
ros2 launch mapping_localization_python mapping_pipeline.launch.py \
use_synthetic_perception:=true \
use_kitware_slam:=trueBy default, this launch passes:
mapping_localization_python/config/kitware_slam_params.yaml
to lidar_slam_node (configured for odometry_frame: map and pose output at
20 Hz, keypoint map outputs enabled, and empty initial map prefix). Replace it with your tuned config when available:
ros2 launch mapping_localization_python mapping_pipeline.launch.py \
use_kitware_slam:=true \
kitware_slam_params:=/absolute/path/to/your_slam_params.yamlTune SLAM pose output frame and rate:
ros2 launch mapping_localization_python mapping_pipeline.launch.py \
use_kitware_slam:=true \
slam_pose_frame:=map \
slam_pose_publish_rate_hz:=20.0Override the default distance topic if needed:
ros2 launch mapping_localization_python mapping_pipeline.launch.py \
output_distance_topic:=/ap1/localization/distanceTune /slam_map output or enable the optional occupancy grid:
ros2 launch mapping_localization_python mapping_pipeline.launch.py \
use_kitware_slam:=true \
slam_map_publish_rate_hz:=0.5 \
publish_slam_map_grid:=truePreload a saved Kitware map prefix for localization mode:
ros2 launch mapping_localization_python mapping_pipeline.launch.py \
use_kitware_slam:=true \
kitware_initial_maps_path:=/absolute/path/to/slam_mapSave the current map:
ros2 service call /save_map std_srvs/srv/Empty