DEV Community

Md Shaifur Rahman
Md Shaifur Rahman

Posted on

Fleet-Scale Robotics: Reliable USB Device Binding on NVIDIA Jetson Orin

If you have ever built an autonomous mobile robot, you have likely run into the dreaded "Shuffled USB Port" problem.

You boot up your robot, fire up your ROS 2 launch files, and... crash. Your LiDAR driver is trying to parse data from your IMU, and your IMU node is screaming about invalid serial frames. Because Linux assigns virtual serial paths like /dev/ttyUSB0 and /dev/ttyUSB1 based purely on which device initialized milliseconds faster, relying on default OS paths is a recipe for system instability.

When you are scaling up to dozens of Jetson Orin nodes—each equipped with an RPLIDAR C1 and a Yahboom 10-axis IMU—manually hardcoding paths or writing rigid scripts on every individual machine isn't viable.

Here is how production-grade robotics fleets handle plug-and-play USB binding dynamically using configuration-driven udev rules.


The Core Concept: Vendor ID vs. Physical Port vs. Serials

Linux's udev (device manager) allows us to dynamically create stable symbolic links (symlinks) like /dev/rplidar and /dev/imu when hardware is plugged in. How we identify those devices determines our fleet's flexibility:

  • USB Serials: Unique to each individual chip. Highly secure, but requires registering every single replacement sensor in your codebase.
  • Physical USB Ports (KERNELS): Tied to a physical slot on the carrier board. Great if you have identical sensors, but forces technicians to plug cables into highly specific, undocumented ports.
  • Vendor ID (VID) & Product ID (PID): Identifies the USB-to-serial converter chip on the sensor board.

Because the RPLIDAR C1 uses a Silicon Labs CP210x chip (10c4:ea60) and the Yahboom IMU uses a QinHeng CH340 chip (1a86:7523), they use completely distinct silicon. This means we can map them dynamically and reliably using just their VID/PID—allowing field technicians to plug them into any USB port on the Jetson without breaking the system.


Step 1: The Configuration-Driven File (devices.conf)

Hardcoding vendor rules inside shell scripts or configuration managers lacks flexibility. Instead, we separate our hardware definitions from our setup logic.

Create a file named devices.conf:

# symlink_name,vendor_id,product_id
lidar,10c4,ea60
imu,1a86,7523
Enter fullscreen mode Exit fullscreen mode

This simple, comma-separated format can be easily parsed by our installation scripts, checked into Git, and updated if we change sensors down the line.

Step 2: The Automated Provisioning Script

This Bash script reads devices.conf line-by-line, dynamically generates a /etc/udev/rules.d/99-robot-usb.rules file, handles user permission groupings, applies the rules, and verifies the symlinks.

Save the following as setup_usb.sh:

#!/usr/bin/env bash

# Exit immediately if a command fails
set -e

CONF_FILE="devices.conf"
RULE_FILE="/etc/udev/rules.d/99-robot-usb.rules"

echo "=========================================================="
echo "    Jetson Fleet Provisioning: Config File Reader"
echo "=========================================================="

# 1. Verify the config file exists
if [ ! -f "$CONF_FILE" ]; then
    echo "ERROR: Configuration file '$CONF_FILE' not found!"
    exit 1
fi

# 2. Write the rules to a temporary file first
TMP_RULES=$(mktemp)
echo "# Auto-generated udev rules from $CONF_FILE" > "$TMP_RULES"
echo "# Do not edit this file directly. Edit $CONF_FILE instead." >> "$TMP_RULES"
echo "" >> "$TMP_RULES"

echo "Reading configurations from $CONF_FILE..."

# Parse the config file line by line
while IFS=, read -r name vid pid || [ -n "$name" ]; do
    # Strip leading/trailing whitespace using xargs
    name=$(echo "$name" | xargs)
    vid=$(echo "$vid" | xargs)
    pid=$(echo "$pid" | xargs)

    # Skip comments and empty lines
    if [[ -z "$name" || "$name" == \#* ]]; then
        continue
    fi

    echo "  → Configured mapping: /dev/$name (VID: $vid, PID: $pid)"

    # Write the formatted udev rule to our temp file
    echo "# $name mapping rule" >> "$TMP_RULES"
    echo "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"$vid\", ATTRS{idProduct}==\"$pid\", SYMLINK+=\"$name\", MODE=\"0666\"" >> "$TMP_RULES"
    echo "" >> "$TMP_RULES"

done < "$CONF_FILE"

# 3. Copy the finished rules file to the secure system directory
sudo cp "$TMP_RULES" "$RULE_FILE"
rm "$TMP_RULES"

# 4. Ensure user is in dialout group
echo "Adding user '$USER' to the 'dialout' group..."
sudo usermod -aG dialout "$USER"

# 5. Reload and trigger the system rules
echo "Applying new udev rules..."
sudo udevadm control --reload-rules
sudo udevadm trigger

echo "----------------------------------------------------------"
echo "SUCCESS: Rules applied!"
echo "----------------------------------------------------------"
echo "Verifying device bindings based on $CONF_FILE:"

# 6. Read the config again to dynamically verify if the links successfully bound
while IFS=, read -r name vid pid || [ -n "$name" ]; do
    name=$(echo "$name" | xargs)
    if [[ -z "$name" || "$name" == \#* ]]; then
        continue
    fi

    if [ -L "/dev/$name" ]; then
        echo "   ✓ Device successfully mapped to -> /dev/$name"
    else
        echo "   ✗ /dev/$name is NOT active. Is this device plugged in?"
    fi
done < "$CONF_FILE"

echo "=========================================================="
echo "NOTE: Close this SSH session and log back in to apply"
echo "the dialout group permission updates."
echo "=========================================================="
Enter fullscreen mode Exit fullscreen mode

Make the script executable and run it on your Jetson:

chmod +x setup_usb.sh
./setup_usb.sh
Enter fullscreen mode Exit fullscreen mode

Step 3: Integrating with ROS 2 Launch Files

Now that your OS guarantees your devices will always mount at /dev/lidar and /dev/imu with read/write (0666) permissions, we can configure our ROS 2 launch configurations safely.

Furthermore, production-grade drivers should always utilize automatic reconnection and node respawn parameters to handle transient power drops or physical cable vibrations during navigation.

# rplidar_launch_example.py excerpt
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='rplidar_ros',
            executable='rplidar_composition',
            output='screen',
            respawn=True,         # Auto-restart the node if it crashes
            respawn_delay=2.0,    # Wait 2 seconds before retrying
            parameters=[{
                'serial_port': '/dev/lidar', # Map directly to the udev symlink
                'serial_baudrate': 460800,
                'frame_id': 'laser_frame'
            }]
        )
    ])
Enter fullscreen mode Exit fullscreen mode

Fleet Deployment & Ansible Automation

For larger scale operations (dozens of Jetsons), running this script manually on every single node is inefficient. Since the generated /etc/udev/rules.d/99-robot-usb.rules is purely generic (it targets chip models, not serial numbers), we can easily push the file to all hosts using an Ansible playbook task:

- name: Push universal udev rules to all robots
  copy:
    src: files/99-robot-usb.rules
    dest: /etc/udev/rules.d/99-robot-usb.rules
    owner: root
    group: root
    mode: '0644'
  notify: Reload Udev Daemon
Enter fullscreen mode Exit fullscreen mode

By transitioning to configuration-driven hardware mapping, you remove physical port dependencies, simplify your disk cloning pipeline, and prevent silent data mixups across your fleet. Happy hacking!

Top comments (0)