00

Engineering Objective

Develop a ROS2 LiDAR pipeline for reading data, recording it in CSV format, and detecting objects in proximity.

This modeule is intended to be integrated to MicroChariot's object avoidance protocol during its navigation.

OBJECTIVE Read and stream LiDAR data reliably
01
ROS 2 Integration

LiDAR Publisher / Subscriber Architecture

ROS 2 organizes robotic systems into independent nodes that communicate through named topics. The LiDAR driver publishes each scan as a sensor_msgs/LaserScan message on /ldlidar_node/scan.

I created a subscriber node that listens to this topic and processes each incoming scan. This separates the LiDAR hardware interface from the data-processing layer and allows the same sensor stream to be used by multiple ROS 2 nodes.

ROS 2 LiDAR publisher and subscriber architecture
ROS 2 data flow from the LiDAR hardware to the subscriber node.
LIDAR SUBSCRIBER ROS 2 / PYTHON
self.subscription = self.create_subscription(
    LaserScan,
    '/ldlidar_node/scan',
    self.listener_callback,
    10
)
LIDAR SCAN REPRESENTATION
θᵢ = θₘᵢₙ + iΔθ
Pᵢ = (θᵢ, rᵢ)

A LaserScan contains an ordered set of range measurements. The angle corresponding to each range is reconstructed from the starting angle and angular increment contained in the ROS message.

Invalid measurements such as infinity and NaN values are filtered out, leaving valid angle-range pairs for later visualization, mapping, or obstacle processing.

02
ROS 2 Node Management

Lifecycle Control & LiDAR Data Logging

The LiDAR driver is managed as a ROS 2 lifecycle node. My script launches the driver, explicitly transitions it through configuration and activation, and then begins listening for scan data.

When the program exits, the process reverses those transitions and terminates the LiDAR launch process. This creates a controlled startup and shutdown sequence instead of leaving the sensor node running in an undefined state.

ROS 2 lifecycle and LiDAR data logging workflow
Polar coordinate scan of distances, the original format before switching to CSV for speed.
LIFECYCLE SEQUENCE
Launch → Configure → Activate → Acquire Data → Deactivate → Shutdown

The lifecycle transitions allow the sensor driver to be initialized before data acquisition begins and cleanly stopped when the program exits.

LIDAR LIFECYCLE ROS 2 / PYTHON
lidar_proc = subprocess.Popen([
    "ros2",
    "launch",
    "ldlidar_node",
    "ldlidar.launch"
])

subprocess.run([
    "ros2",
    "lifecycle",
    "set",
    "/ldlidar_node",
    "configure"
])

subprocess.run([
    "ros2",
    "lifecycle",
    "set",
    "/ldlidar_node",
    "activate"
])
SLIDING SCAN WINDOW
W = {Sₜ₋₄₉, ..., Sₜ}

Instead of storing only the newest scan, I maintain a rolling buffer of the 50 most recent LaserScan messages. Once the buffer reaches capacity, the oldest scan is automatically removed as a new one arrives.

SCAN BUFFER PYTHON
scan_window = deque(maxlen=50)

...

scan_window.append(scan)
CSV DATA LOGGING
Sᵢ = (i, θLiDAR, θplot, r)

Each scan is converted into angle-range measurements and stored with its scan index. The complete 50-scan window is rewritten to reported_data.csv whenever a new scan arrives.

Both the LiDAR-native angle and a transformed plotting angle are saved, along with the measured range in meters. Invalid ranges are represented as empty CSV entries.

DATA_LOGGER PYTHON / CSV
writer.writerow([
    'scan_index',
    'lidar_deg',
    'plot_deg',
    'range_m'
])

for idx, scan in enumerate(scan_window):

    for lidar_deg, plot_deg, r in scan:

        r_str = '' if r is None else f"{r:.3f}"

        writer.writerow([
            idx,
            f"{lidar_deg:.2f}",
            f"{plot_deg:.2f}",
            r_str
        ])
CONTROLLED SHUTDOWN

A SIGINT handler catches Ctrl+C and runs a common cleanup routine. The routine deactivates and shuts down the lifecycle node, terminates the launch process, and destroys the subscriber node.

CLEANUP ROS 2 / PYTHON
subprocess.run([
    "ros2",
    "lifecycle",
    "set",
    "/ldlidar_node",
    "deactivate"
])

subprocess.run([
    "ros2",
    "lifecycle",
    "set",
    "/ldlidar_node",
    "shutdown"
])

lidar_proc.terminate()
node.destroy_node()
rclpy.shutdown()
03
Proximity Detection

Logging Nearby Objects with a Proximity Radius

After collecting LiDAR scans, I used the range data to identify objects that entered a defined proximity radius around the robot.

Each beam is checked against a distance threshold. Measurements inside that radius are flagged and highlighted in the live polar visualization, providing a simple way to detect nearby obstacles in real time.

PROXIMITY RADIUS
rᵢ < rproximity
rproximity = 0.20 m

Each valid LiDAR range is compared against the proximity threshold. If the measured distance falls below 0.20 m, that beam is flagged as representing a nearby object.

PROXIMITY DETECTION ROS 2 / NUMPY
PROXIMITY_THRESH = 0.2

ranges = np.array(msg.ranges, dtype=float)

valid = (
    (ranges > 0.0)
    & np.isfinite(ranges)
)

nearby = (
    valid
    & (ranges < PROXIMITY_THRESH)
)
STICKY DETECTION

The visualization maintains a boolean flag for each LiDAR beam. Once a beam detects an object inside the proximity radius, that location remains highlighted while the nearby condition persists.

This makes close-range detections easier to track visually than relying on a single instantaneous scan.

PROXIMITY GUI PYTHON
below = (
    valid
    & (ranges < PROXIMITY_THRESH)
)

self.flagged = np.where(
    below,
    True,
    self.flagged
)

flagged_theta = self.angles[self.flagged]
flagged_r = ranges[self.flagged]
LIVE POLAR VISUALIZATION
Pᵢ = (θᵢ, rᵢ)

LiDAR measurements are plotted directly in polar coordinates. Normal scan points show the surrounding environment, while points inside the proximity radius are displayed separately as flagged detections.