devmaker.net
start/ linux/ how-to-create-a-symlink-for-ttyusbx
Linux

How to create a symlink for ttyUSBx?

Everyone who operates a Raspberry Pi with connected USB devices probably knows the problem: After a system restart or when changing the USB port, the designation of the connected USB devices suddenly changes.

Harry_im_Homelab31 (Portrait)
Harald Henning
2022-07-26 · ~7 min read
Stable device names via a udev rule instead of shifting ttyUSBx
Stable device names via a udev rule instead of shifting ttyUSBx
Part of a guide

This article is part of the Homelab Self-Hosting Guide – the curated learning path for your own homelab.

The problem: after a reboot everything is scrambled

Anyone running a Raspberry Pi or homelab server with several USB devices knows this: a Zigbee coordinator stick sits at /dev/ttyUSB0, a Z-Wave stick at /dev/ttyUSB1. Everything works – until the next reboot. Suddenly the numbers are swapped, Zigbee2MQTT talks to the Z-Wave stick, and the smart home is upside down.

The cause: Linux assigns the ttyUSB numbers in the order the USB devices are detected at boot. That order isn't deterministic. The fix is a udev rule that gives each device a fixed symlink name – set it up once, never think about it again.

Ad · Affiliate link – if you buy through it, I may earn a commission. It doesn’t change the price for you.

Short version

Read the USB device's vendor and product ID with udevadm info, create a rule in /etc/udev/rules.d/, run udevadm trigger – done. From then on the device is reachable under a fixed name like /dev/ttyZCOORD.

Identify the USB device

First we need the device's unique IDs. Plug in the stick and read the attributes:

bash
udevadm info --name=/dev/ttyUSB0 --attribute-walk

The output is long – we're looking for idVendor and idProduct. This combination uniquely identifies the device type:

bash
  looking at parent device '/devices/platform/.../usb1/1-1/1-1.2':
    KERNELS=="1-1.2"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{bDeviceClass}=="00"
    ATTRS{bMaxPower}=="100mA"
    ATTRS{idProduct}=="ea60"
    ATTRS{idVendor}=="10c4"
    ATTRS{manufacturer}=="Silicon Labs"
    ATTRS{product}=="slae.sh cc2652rb stick - slaesh's iot stuff"

In this case: idVendor="10c4" and idProduct="ea60" – a Silicon Labs CP2102-based Zigbee stick. Note the values, we'll need them in a moment.

Create the udev rule

Now we create a rule file that automatically creates a fixed symlink on plug-in or at boot:

bash
sudo nano /etc/udev/rules.d/98-usb-device.rules

File contents – replace idVendor and idProduct with your own values; SYMLINK is the name the device should be reachable under from now on:

bash
KERNEL=="ttyUSB?", SUBSYSTEMS=="usb", ATTRS{idProduct}=="ea60", ATTRS{idVendor}=="10c4", SYMLINK+="ttyZCOORD"
Careful with multiple identical sticks

If you have two USB devices with identical vendor/product IDs (e.g. two Silicon Labs sticks), this method alone isn't enough. You then also have to filter by ATTRS{serial} or the physical USB port (KERNELS). Otherwise both end up on the same symlink.

Apply and test the rule

Load the new rules – a full reboot isn't necessary:

bash
sudo udevadm trigger

Check whether the symlink is there:

bash
ls -l /dev/ttyZ*
lrwxrwxrwx  1 root root  7 28. Jun 00:17 /dev/ttyZCOORD -> ttyUSB0

From now on /dev/ttyZCOORD always points to the right stick – no matter which ttyUSB number Linux gives it at boot. In Zigbee2MQTT, Z-Wave JS or ESPHome, just enter the symlink as the device path.

Where this matters in the homelab

This pattern comes up for me all the time:

  • Zigbee coordinator (CC2652, SkyConnect) → /dev/ttyZCOORD
  • Z-Wave stick (Aeotec Z-Stick) → /dev/ttyZWAVE
  • ESP32 flasher via UART → /dev/ttyESPFLASH

Especially when several sticks hang off the same host at once, this is the only sensible solution. Without udev rules, every reboot is a gamble.

What I left out

Honest boundaries

A few things deliberately not covered here:

  • USB devices without a ttyUSB interface (e.g. pure HID devices) – a different udev mechanism applies there.
  • systemd dependencies. If a service expects the symlink at boot before udev runs the rule, you need an After= dependency. That's a topic of its own.
  • Docker passthrough. If the stick is used inside a Docker container, you additionally have to map --device /dev/ttyZCOORD into the container.

Conclusion

A udev rule is five minutes of work and saves you years of debugging after every reboot. For every USB device you run permanently – Zigbee, Z-Wave, GPS, serial adapter – set up a fixed symlink. It's one of those "why didn't I do this right away" moments.

// More recommendations

Ad · Affiliate link – if you buy through it, I may earn a commission. It doesn’t change the price for you.

// related posts

> echo "your thoughts" >> how-to-create-a-symlink-for-ttyusbx.responses

Post your comment

Required for comment verification