Understanding udev rules
Rules on writing udev rules with a walk-through example
I have been writing udev rules for years and frankly didn't have a full
understanding of it. This time, I took a deep dive to understand core
principles in writing those rules.
When would you want to write your own rules?
- When multiple devices are connected to the system, their names get decided
by the order of the devices being detected (e.g.
/dev/ttyUSB0,/dev/ttyUSB1). If you have a program that wants to refer to a device interface, then it will be unreliable to use their generic name. Here is when you can write a udev rule. You can say, "if device in subsystemttywith vendor ID of xx and product ID of yy gets connected, then create a symlink,/dev/custom/camera". Or, you could also just name the device, rather than creating a symbolic link for it. - Rename a device to a more unique name
- And more, as described here
Writing udev rules
What is udev?
“The udev daemon manages the /dev filesystem. It allows you to identify devices based on their properties, such as vendor ID or device ID, dynamically." (source)
Where to write udev rules
“The main configuration file is in /etc/udev/udev.conf, and rules are read from
/run/udev/rules.d, /etc/udev/rules.d and /lib/udev/rules.d: If a file with
the same basename is present in more than one of these directories the later
versions are be ignored. The idea is that packages should install rules in
/lib/udev/rules.d), and the administrator can overrides these by putting files in
/etc (or /run for temporary experiments)” (source)
How to write udev rules
-
use
dmesgto find where the device mounted (e.g. /dev/ttyUSB0)- hot-plug the device and use
dmesg -w
- hot-plug the device and use
-
use udevadm info to find udev attributes
udevadm info --name=/dev/ttyUSB0 --attribute-walk
-
write rules that will allow udev to target device(s) you want
- rules are written in:
/etc/udev/rules.dor/lib/udev/rules.d. Then where should you place the rules?/etc/udev/rules.d: system/admin-level rules (these rules are applied last, meaning it will override rules in/lib/udev/rules.dif the rules are conflicting each other)./lib/udev/rules.d: package-level rules
- note that you cannot combine attributes from multiple parent devices (refer to the example below)
- rules are written in:
-
to apply the rules, reload udev rules with the following command:
sudo udevadm control --reload-rules && sudo udevadm trigger
Walk-through Example
Say, you want to create a symlink for a new device from SIPEED (manufacturer) maixsense.
First you want to find out how it’s detected by default so that you can use this
information to get more device info. Monitor dmesg dmesg -w when plugging the
device into the USB port. Something like below will show up:
[21594.205774] usb 7-1.3: new full-speed USB device number 10 using xhci_hcd
[21594.302843] usb 7-1.3: New USB device found, idVendor=0403, idProduct=6010, bcdDevice= 5.00
[21594.302873] usb 7-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[21594.302881] usb 7-1.3: Product: SIPEED Meta Sense Lite
[21594.302889] usb 7-1.3: Manufacturer: SIPEED
[21594.302896] usb 7-1.3: SerialNumber: 202206 B79613
[21594.307906] ftdi_sio 7-1.3:1.0: FTDI USB Serial Device converter detected
[21594.308036] usb 7-1.3: Detected FT2232C
[21594.311100] usb 7-1.3: FTDI USB Serial Device converter now attached to ttyUSB1
[21594.311507] ftdi_sio 7-1.3:1.1: FTDI USB Serial Device converter detected
[21594.311623] usb 7-1.3: Detected FT2232C
[21594.313590] ftdi_sio ttyUSB2: Unable to read latency timer: -5
[21594.314318] usb 7-1.3: FTDI USB Serial Device converter now attached to ttyUSB2This one is interesting because one device has two interfaces: ttyUSB1 and ttyUSB2
One interface streams data that we need, and the other is a debug port.
Our goal is to write a rule that will correctly identify the interface that
streams data, and create a symlink (or rename if you want) to allow different
applications to access data streamed from the sensor via uniquely named interface,
say, /dev/custom/tof_sensor.
First, you need to find out which interface is for data streaming and which for
debugging. You can use tools like hexdump to see if data is being streamed:
hexdump -C /dev/ttyUSB1Upon running hexdump on both interfaces, it was identified that:
/dev/ttyUSB1streams something -> this must be a data stream port/dev/ttyUSB2does not show anything when usinghexdump-> this must be a debug port
Now, we are ready to use udevadm info to find attributes that can distinguish
one from another (basically you are looking for a set of “rules” that would allow
udev to match to the exact device interface) by running udevadm info on both
interfaces. The below shows the example results of running udevadm info on
interface, /dev/ttyUSB1:
$ udevadm info --name /dev/ttyUSB1 -a
Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.
# This would be the actual USB interface - doesn't have much info
looking at device '/devices/platform/fe190000.pcie/pci0004:40/0004:40:00.0/0004:41:00.0/usb7/7-1/7-1.3/7-1.3:1.0/ttyUSB1/tty/ttyUSB1':
KERNEL=="ttyUSB1"
SUBSYSTEM=="tty"
DRIVER==""
ATTR{power/async}=="disabled"
ATTR{power/control}=="auto"
... (abbreviated since we don't need them)
# Parent of the actual USB interface
looking at parent device '/devices/platform/fe190000.pcie/pci0004:40/0004:40:00.0/0004:41:00.0/usb7/7-1/7-1.3/7-1.3:1.0/ttyUSB1':
KERNELS=="ttyUSB1"
SUBSYSTEMS=="usb-serial"
DRIVERS=="ftdi_sio"
ATTRS{event_char}=="(not readable)"
ATTRS{latency_timer}=="16"
ATTRS{port_number}=="0"
... (abbreviated since we don't need them)
# "Grandparent" of the actual USB interface
looking at parent device '/devices/platform/fe190000.pcie/pci0004:40/0004:40:00.0/0004:41:00.0/usb7/7-1/7-1.3/7-1.3:1.0':
KERNELS=="7-1.3:1.0"
SUBSYSTEMS=="usb"
DRIVERS=="ftdi_sio"
ATTRS{authorized}=="1"
ATTRS{bAlternateSetting}==" 0"
ATTRS{bInterfaceClass}=="ff"
ATTRS{bInterfaceNumber}=="00"
ATTRS{bInterfaceProtocol}=="ff"
ATTRS{bInterfaceSubClass}=="ff"
ATTRS{bNumEndpoints}=="02"
... (abbreviated since we don't need them)
# This would be the actual device - note: idProduct and idVendor; and SIPEED
# is the manufacturer of maixsense; "Great grandparent"
looking at parent device '/devices/platform/fe190000.pcie/pci0004:40/0004:40:00.0/0004:41:00.0/usb7/7-1/7-1.3':
KERNELS=="7-1.3"
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{authorized}=="1"
ATTRS{avoid_reset_quirk}=="0"
ATTRS{bConfigurationValue}=="1"
ATTRS{bDeviceClass}=="00"
ATTRS{bDeviceProtocol}=="00"
ATTRS{bDeviceSubClass}=="00"
ATTRS{bMaxPacketSize0}=="64"
ATTRS{bMaxPower}=="90mA"
ATTRS{bNumConfigurations}=="1"
ATTRS{bNumInterfaces}==" 2"
ATTRS{bcdDevice}=="0500"
ATTRS{bmAttributes}=="a0"
ATTRS{busnum}=="7"
ATTRS{configuration}==""
ATTRS{devnum}=="5"
ATTRS{devpath}=="1.3"
ATTRS{devspec}=="(null)"
ATTRS{idProduct}=="6010"
ATTRS{idVendor}=="0403"
ATTRS{ltm_capable}=="no"
... (abbreviated since we don't need them)
# below are parent device of the maixsense device (omitted)
...
Useful tip: you can search idVendor and idProduct on this website to find which vendor and which product the device is from this website
You would run udevadm info --name /dev/ttyUSB2 -a and will find that attributes that allow
you to tell the two interfaces apart is bInterfaceNumber, which is defined as a parent
interface.
/dev/ttyUSB1has the attribute:ATTRS{bInterfaceNumber}=="00"/dev/ttyUSB0has the attribute:ATTRS{bInterfaceNumber}=="01"This means we can usebInterfaceNumberin our udev rule.
Let’s look at how we can define the rules. Our strategy is:
- Rule #1: identify the tof sensor by using
idVendorandidProduct- this will yield two matching interfaces
- Rule #2: use
bInterfaceNumberto distinguish between the two interfaces
First attempt would be:
# this is from the child device
SUBSYSTEM=="tty", \
# Below defines "rule #1" as udev rules:
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", \
# Below defines "rule #2" as udev rules:
ATTRS{bInterfaceNumber}=="00" \
# requests to create a symlink to the device, `/dev/custom/tof_sensor`
SYMLINK+="custom/tof_sensor" \
When you run "reload", you will see that no symlink to the device is created. Why would that be? It's because,
You can use attributes from only the child device and a single parent device.
The rule above mixes attributes from different parent devices: idVendor and
idProduct from "great grandparent" device, and bInterfaceNumber from
"grandparent" device.
Then what now? How can you create a udev rule that will allow matching rules from multiple parents?
You can introduce a new environment variable and do as below:
SUBSYSTEM=="tty", \
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", \
ENV{MAIXSENSE_DEV}="1"
# EOL; sets "MAIXSENSE_DEV" to 1, when matching attributes found
ENV{MAIXSENSE_DEV}=="1", \
ATTRS{bInterfaceNumber}=="00", \
SYMLINK+="custom/tof_sensor"
When you "reload", you can see that the symlink to the desired interface is created! Yay!
References
Really good resources on (you should read them!):
- what udev is: udev - Debian Wiki
- how to write udev rules: writing udev rules