0. Package Structure
laser_subscriber/
├── launch/
│ └── laser_subscriber_launch.py
├── config/
│ └── rviz_laser_subscriber.rviz
│ └── params.yaml
├── src/
│ └── laser_subscriber.cpp
├── CMakeLists.txt
├── package.xml
1. Create a package
$ cd ~/robot_ws/src/
$ ros2 pkg create --build-type ament_cmake laser_subscriber --dependencies rclcpp sensor_msgs pcl_conversions pcl_msgs
2. C++ source code (src/laser_subscriber.cpp)
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/point_cloud2.hpp>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pub_laser;
double voxel_leaf_size = 0.1;
void cloud_callback(const sensor_msgs::msg::PointCloud2::SharedPtr laser)
{
RCLCPP_INFO(
rclcpp::get_logger("laser_subscribe_node"),
"Subscribed laser! Voxel leaf size = %.2f",
voxel_leaf_size
);
// ROS → PCL
pcl::PointCloud<pcl::PointXYZ>::Ptr input_cloud(new pcl::PointCloud<pcl::PointXYZ>());
pcl::fromROSMsg(*laser, *input_cloud);
pcl::PointCloud<pcl::PointXYZ>::Ptr output_cloud(new pcl::PointCloud<pcl::PointXYZ>());
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(input_cloud);
voxel.setLeafSize(voxel_leaf_size, voxel_leaf_size, voxel_leaf_size);
voxel.filter(*output_cloud);
// PCL → ROS
sensor_msgs::msg::PointCloud2 output;
pcl::toROSMsg(*output_cloud, output);
output.header = laser->header;
pub_laser->publish(output);
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("laser_subscribe_node");
// Declare parameters with defaults
node->declare_parameter<double>("voxel_leaf_size", 5.0);
// Get params
node->get_parameter("voxel_leaf_size", voxel_leaf_size);
// Publisher
pub_laser = node->create_publisher<sensor_msgs::msg::PointCloud2>("/ouster/voxel", 10);
// Subscriber
auto sub = node->create_subscription<sensor_msgs::msg::PointCloud2>(
"/ouster/points", 10, cloud_callback);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
3. CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(laser_subscriber)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(pcl_conversions REQUIRED)
find_package(pcl_msgs REQUIRED)
find_package(PCL REQUIRED COMPONENTS common filters io)
include_directories(${PCL_INCLUDE_DIRS})
add_executable(laser_subscriber_node src/laser_subscriber.cpp)
ament_target_dependencies(laser_subscriber_node
rclcpp
sensor_msgs
pcl_conversions
pcl_msgs
)
target_link_libraries(laser_subscriber_node
${PCL_COMMON_LIBRARIES}
${PCL_FILTERS_LIBRARIES}
${PCL_IO_LIBRARIES}
)
install(TARGETS
laser_subscriber_node
DESTINATION lib/${PROJECT_NAME}
)
install(DIRECTORY launch config
DESTINATION share/${PROJECT_NAME}/
)
ament_package()
4. rviz configuration
locate ‘rviz_laser_subscriber.rviz’ in ‘config’ folder
5. params.yaml file
laser_subscriber:
ros__parameters:
voxel_leaf_size: 0.5
6. launch file
from launch import LaunchDescription
from launch_ros.actions import Node
import os
def generate_launch_description():
pkg_name = 'laser_subscriber'
pkg_dir = os.popen('/bin/bash -c "source /usr/share/colcon_cd/function/colcon_cd.sh && \
colcon_cd %s && pwd"' % pkg_name).read().strip()
return LaunchDescription([
Node(
package='laser_subscriber',
executable='laser_subscriber_node',
name='laser_subscriber',
output='screen',
parameters= [os.path.join(pkg_dir, 'config', 'params.yaml')]
),
Node(
package='rviz2',
namespace='',
executable='rviz2',
name='rviz2',
arguments=['-d', [os.path.join(pkg_dir, 'config', 'rviz_laser_subscriber.rviz')]]
)
])
7. Run
$ colcon build --packages-select laser_subscriber
$ source install/setup.bash
$ ros2 launch laser_subscriber laser_subscriber_launch.py
8. Play ROS Bag File
$ ros2 bag play {bagfile}
9. Results

Read voxel filter parameters from ‘config/params.yaml’ and publish laser topic