I’ve been playing with udev rules for a couple of days and I’ve been quite interested in the capabilities it offers.

I highly recommend this two documents:

This guide reflects the whole process from connecting the device, identifying the events in linux, processing udev rules and executing an script every time the device is connected/disconnected.

Scenario:

  • Notebook: HP 820 g3
  • Dock:  2013 Ultra Slim Docking Station (two display ports)
  • Notebook ports: (1) Display Port and (1) VGA ports.

My objective was to run a bash script every time a display port cable  is connected/disconnected in any of those ports (with my Dock).

Detecting devices/events at kernel’s level

Kernel will be the first to be notified when something is connected, in this stage we can detect almost anything, any new device (USB Pendrives, Monitors or whatever we connect) usually will be detected and informed to the kernel.

This is the result of connecting one Display Port cable (attached to a monitor) to my dock:

root@lubuntu:~# udevadm monitor -p

UDEV  [362615.666734] change   /devices/pci0000:00/0000:00:02.0/drm/card0 (drm)
ACTION=change
DEVNAME=/dev/dri/card0
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0
DEVTYPE=drm_minor
HOTPLUG=1
ID_FOR_SEAT=drm-pci-0000_00_02_0
ID_PATH=pci-0000:00:02.0
ID_PATH_TAG=pci-0000_00_02_0
MAJOR=226
MINOR=0
SEQNUM=3084
SUBSYSTEM=drm
TAGS=:master-of-seat:uaccess:seat:
USEC_INITIALIZED=5157028
net.ifnames=0

KERNEL[362616.133667] add      /devices/pci0000:00/0000:00:02.0/i2c-6/i2c-dev/i2c-6 (i2c-dev)
ACTION=add
DEVNAME=/dev/i2c-6
DEVPATH=/devices/pci0000:00/0000:00:02.0/i2c-6/i2c-dev/i2c-6
MAJOR=89
MINOR=6
SEQNUM=3101
SUBSYSTEM=i2c-dev

KERNEL[362616.133849] add      /devices/pci0000:00/0000:00:02.0/i2c-6 (i2c)
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:02.0/i2c-6
SEQNUM=3102
SUBSYSTEM=i2c

KERNEL[362616.134155] add      /devices/pci0000:00/0000:00:02.0/drm/card0/card0-DP-3 (drm)
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0/card0-DP-3
SEQNUM=3103
SUBSYSTEM=drm

KERNEL[362616.134348] change   /devices/pci0000:00/0000:00:02.0/drm/card0 (drm)
ACTION=change
DEVNAME=/dev/dri/card0
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0
DEVTYPE=drm_minor
HOTPLUG=1
MAJOR=226
MINOR=0
SEQNUM=3104
SUBSYSTEM=drm

KERNEL[362616.134486] add      /devices/pci0000:00/0000:00:02.0/drm/card0/card0-DP-4 (drm)
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0/card0-DP-4
SEQNUM=3105
SUBSYSTEM=drm

KERNEL[362616.134651] change   /devices/pci0000:00/0000:00:02.0/drm/card0 (drm)
ACTION=change
DEVNAME=/dev/dri/card0
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0
DEVTYPE=drm_minor
HOTPLUG=1
MAJOR=226
MINOR=0
SEQNUM=3106
SUBSYSTEM=drm

KERNEL[362616.134781] add      /devices/pci0000:00/0000:00:02.0/drm/card0/card0-DP-5 (drm)
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0/card0-DP-5
SEQNUM=3107
SUBSYSTEM=drm

KERNEL[362616.134934] change   /devices/pci0000:00/0000:00:02.0/drm/card0 (drm)
ACTION=change
DEVNAME=/dev/dri/card0
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0
DEVTYPE=drm_minor
HOTPLUG=1
MAJOR=226
MINOR=0
SEQNUM=3108
SUBSYSTEM=drm

KERNEL[362616.135128] change   /devices/pci0000:00/0000:00:02.0/drm/card0 (drm)
ACTION=change
DEVNAME=/dev/dri/card0
DEVPATH=/devices/pci0000:00/0000:00:02.0/drm/card0
DEVTYPE=drm_minor
HOTPLUG=1
MAJOR=226
MINOR=0
SEQNUM=3109
SUBSYSTEM=drm

Woah, almost 10 events were triggered, so now which one we would choose?

UDEV:

  • Detected 1 (one) change in the parent device card0

KERNEL:

  • Detected 4 (four) changes
    • The 4 (four) changes are for the parent device card0
  • Detected 5 (five) add events
    • 2 (two) added for /dev/i2c-6
      • One for the subsystem: i2c-dev
      • One for the subsystem: i2c
    • 3 (three) for added display ports:
      • One for card0-DP-3
      • One for card0-DP-4
      • One for card0-DP-5

So in order to select an specific event I will choose the subsystem i2c-dev, this will be the event that will trigger the script in the UDEV rule:

KERNEL[362616.133667] add      /devices/pci0000:00/0000:00:02.0/i2c-6/i2c-dev/i2c-6 (i2c-dev)
ACTION=add
DEVNAME=/dev/i2c-6
DEVPATH=/devices/pci0000:00/0000:00:02.0/i2c-6/i2c-dev/i2c-6
MAJOR=89
MINOR=6
SEQNUM=3101
SUBSYSTEM=i2c-dev

We will take the KERNEL event «add», the DEVNAME «/dev/i2c-6» and the SUBSYSTEM: «i2c-dev» because it happens only ONCE when I connect the DP cable. I didn’t used the UDEV rule for the parent device card0, after several testing the «change» event for card0 happens many times when disconnecting the cable, so it isn’t an option.

Remember selecting an specific event and it’s properties, otherwise the script will ran multiple times and that would lead to unwanted results.

Well, now let’s take a look on xrandr to see which monitor was attached:

root@lubuntu:~# xrandr
Screen 0: minimum 8 x 8, current 1600 x 900, maximum 32767 x 32767
eDP1 connected 1366x768+0+0 (normal left inverted right x axis y axis) 277mm x 156mm
   1366x768      60.06*+  40.04  
   1360x768      59.80    59.96  
   1280x720      60.00  
   1024x768      60.00  
   1024x576      60.00  
   960x540       60.00  
   800x600       60.32    56.25  
   864x486       60.00  
   640x480       59.94  
   720x405       60.00  
   680x384       60.00  
   640x360       60.00  
DP1 disconnected (normal left inverted right x axis y axis)
DP1-1 connected 1600x900+0+0 (normal left inverted right x axis y axis) 443mm x 249mm
   1600x900      60.00*+
   1280x1024     60.02  
   1440x900      59.89  
   1280x800      59.81  
   1280x720      60.00  
   1024x768      60.00  
   800x600       60.32  
   640x480       60.00  
   720x400       70.08  
DP1-2 disconnected (normal left inverted right x axis y axis)
DP1-3 disconnected (normal left inverted right x axis y axis)
DP2 disconnected (normal left inverted right x axis y axis)
HDMI1 disconnected (normal left inverted right x axis y axis)
HDMI2 disconnected (normal left inverted right x axis y axis)
VIRTUAL1 disconnected (normal left inverted right x axis y axis)

As you can see, depending on which display port I connect the cable the respective device will be presented to xrandr, it could be DP1-1, DP1-2 or DP1-3, this must be considered when writting the bash script at the end of this document. eDP1 is the default screen.

Setting up udev rules

The secret of a good rule is to be quite specific. That means pointing to the child devices instead of the parent devices. We already did that when we selected the set of events (KERNEL event «add», the DEVNAME «/dev/i2c-6» and the SUBSYSTEM: «i2c-dev») in the first step, because it only happens once.

Now that we know our trigger, it’s time to set up a rule, I’ve created the file local.rules: /etc/udev/rules.d/local.rules

KERNEL=="i2c-[0-9]",ACTION=="add",SUBSYSTEM=="i2c-dev", RUN+="/usr/local/bin/monitor.sh"

Because I don’t know if the device name could change (when rebooting or reconnecting the dock to the laptop) I’ve used the bash matching listing «[0-9]» so this evaluate that one of the characters between brackets is a match, of course our i2c-6 will be a match.

Udev supports many metacharacters besides matching or listing, take a look here.

The rest of the rules are the same we found previously, finally we execute the script when the event «add» for the device i2c-6 with the subsystem i2c-dev occurs.

I’ve also added a rule when the cable is disconnected from the dock:

KERNEL=="i2c-[0-9]",ACTION=="remove",SUBSYSTEM=="i2c-dev", RUN+="/usr/local/bin/monitor.sh"

But that would lead to the script being executed the same way when I connect or disconnect the cable, so I fixed that problem by passing parameters to the script:

KERNEL=="i2c-[0-9]",ACTION=="add",SUBSYSTEM=="i2c-dev", RUN+="/usr/local/bin/monitor.sh 'add'"
KERNEL=="i2c-[0-9]",ACTION=="remove",SUBSYSTEM=="i2c-dev", RUN+="/usr/local/bin/monitor.sh 'remove'"

Finally the bash script. As I’ve mentioned before the monitor could be «DP1-1, DP1-2 or DP1-3»

#!/bin/bash
# miguel.ortiz
# Wait for the kernel to get the new devices
sleep 5 

#Set the screen
export DISPLAY=:0

# udev event
event=$1 # this come from the udev rule (add | remove)

# identify the devices connected 
device=$(xrandr |grep "^DP.* connected" |awk '{print $1}')

# if the event is "add" and a device "exists"
if [ $event == "add" && ${#device[@]} == 1 ] ; then
    xrandr --output $device --auto --output eDP1 --mode 1366x768 --rate 60.06 --same-as $device

# if the event is "remove" and there are no devices left
elif [ $event == "remove" && ${#device[@]} == 0 ] ; then 
    xrandr -s 0

# if something else happens, like connecting two monitors display a window alert
else 
    zenity --info --text "Something odd happened when adding/removing a monitor"

That’s it, now everytime I connect or disconnect display port cable with a monitor it works and when I remove the cable the main screen of the laptop returns to it’s default settings.