
Sharing a small open-source project that integrates a low-cost VL53L0X Time-of-Flight sensor into the Nav2 costmap as an obstacle source, in case it’s useful to anyone working with cheap range sensors on small robots.
Pipeline
VL53L0X --I2C–> ESP32 --USB serial–> serial_bridge → sensor_msgs/Range
→ tof_to_scan → sensor_msgs/LaserScan → nav2_costmap_2d::ObstacleLayer
What’s included
- ESP32 firmware (Arduino, Pololu VL53L0X library) streaming distance over serial with sensor-disconnect recovery
- An rclpy serial-bridge node publishing sensor_msgs/Range, with a threaded reader and automatic serial reconnect
- A Range → LaserScan converter that synthesizes a configurable narrow fan from the single beam (default ±0.15 rad, 31 rays), emitting +inf for out-of-range readings so the obstacle layer can raytrace-clear
- A full Nav2 costmap config wiring the scan into local and global costmaps (marking + clearing, inf_is_valid: true)
- RViz config and a troubleshooting guide
Built and tested on ROS 2 Humble / Ubuntu 22.04. Python packages (ament_python).
Scope / limitations (up front)
This is a supplementary obstacle sensor, not a LiDAR replacement — a single narrow-FOV ToF sees one thin cone.
One thing I’d be curious about others’ experience on: raytrace clearing on a stationary base leaves residual cells at the edges of the cone, since the clearing rays never cross them. It resolves once the robot moves and the cone sweeps, but I’d be interested whether anyone has handled static-case clearing for single-beam sensors more elegantly. Widening the FOV and increasing ray count helped, but felt like a workaround.
A config gotcha that may save someone time: obstacle marking worked but clearing silently didn’t, until I set obstacle_max_range < raytrace_max_range, with raytrace_max_range equal to the sensor’s true range_max. A mismatch there breaks clearing while marking still functions, so it presents as “obstacles never disappear.”
Feedback and suggestions welcome.