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
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 "=========================================================="
Make the script executable and run it on your Jetson:
chmod +x setup_usb.sh
./setup_usb.sh
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'
}]
)
])
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
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)