6

I am trying to automatically run this script, whenever I connect to my bluetooth headset.

I have created the file /etc/udev/rules.d/80-bt-headset.rules with the line

ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50"

but it doesn't do anything. The conditions are fine, a simple test comand is triggered when I enter that instead. The script itself also works fine when run manually.

What is going wrong here?

Update: There is an error when running the script with sudo -u USER (see below for details). Could this be the problem? And how does sudo-ing to the same user break things?

Update 2: After replacing all instances of pacmd with pactl in a2dp.py (and replacing list-sinks with list sinks to make it a valid pactl command), sudo -u USER works, however, the udev rule still doesn't. In /var/log/syslog I just see the line

systemd-udevd[32629]: Process '/home/USER/.local/bin/a2dp_2.py 00:22:37:3D:DA:50' failed with exit code 1.

Update 3 (Solution): The modified skript (pacmd -> pactl, see Update 2) with the environment variables DISPLAY=:0 and XAUTHORITY=/home/USER/.Xauthority did the trick. The udev rule:

ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" ENV{DISPLAY}=":0" ENV{XAUTHORITY}="/home/USER/.Xauthority" RUN+="/home/USER/.local/bin/a2dp_2.py 00:22:37:3D:DA:50"

is working as intended.

(Now the only remaining problem is, that the script itself will trigger the rule, as it reconnects the headset, resulting in an infinite loop. However, that is a separate question, and it should not be too hard to find a workaround. In fact I was expecting that behaviour when I started this thread.)

What works:

  1. The conditions are fine: The line:

    ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/bin/mkdir /tmp/testme"
    

    will create a new directory when I connect to the headset.

  2. The script a2dp.py itself works fine when run from the terminal via

    /home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50
    
  3. Running a simply Python script via udev:

    ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/home/USER/.local/bin/atestscript.py"
    

    where atestscript.py contains:

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    
    import subprocess
    
    def main():
        subprocess.Popen(['mkdir', '/tmp/atestdir'])
    
    if __name__ == '__main__':
        main()
    

    will again create a folder when the device is connected.

What works after replacing pacmd with pactl:

  1. Running the script from the terminal with sudo -u USER or even sudo -u root now works as intended (For the originial script this resulted in:

    USER@MACHINE:~$ sudo -u USER /usr/local/bin/a2dp.py 00:22:37:3D:DA:50
    Connection MADE
    Device MAC: 00:22:37:3D:DA:50
    Command: pacmd list-sinks failed with status: 1
    stderr: No PulseAudio daemon running, or not running as session daemon.
    
    Exiting bluetoothctl
    

What does not work:

  1. Running the script as above or with any of the following lines as the RUN+= part:

    /usr/bin/sudo -u USER /usr/bin/python3 /home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50
    /usr/bin/sudo -u USER /home/USER/.local/bin/a2dp.py 00:22:37:3D:DA:50
    /usr/bin/python3.5 /usr/local/bin/a2dp.py 00:22:37:3D:DA:50
    ENV{DISPLAY}=":0" RUN+="/usr/local/bin/a2dp.py 00:22:37:3D:DA:50"
    

    Even the modified script will not work:

    ENV{DISPLAY}=":0" ENV{PULSE_RUNTIME_PATH}="/run/user/1000/pulse/" RUN+="sudo -u USER /home/USER/.local/bin/a2dp_2.py 00:22:37:3D:DA:50"
    

Further information: udevadm monitor output on connecting to headset:

KERNEL[104388.664737] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
UDEV  [104388.667185] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
KERNEL[104390.848157] add      /devices/virtual/input/input46 (input)
UDEV  [104390.849150] add      /devices/virtual/input/input46 (input)
KERNEL[104390.849471] add      /devices/virtual/input/input46/event17 (input)
UDEV  [104390.864692] add      /devices/virtual/input/input46/event17 (input)
11
  • I don't think headsets belong to input subsystem. You also shouldn't run a script with Python interpreter as command and script as argument. Any valid reason for running it as USER ? Run udev-monitor and try to connect the headset, see what the monitor displays Apr 30, 2017 at 15:28
  • What I also get from the script is that it's supposed to have arguments. Your RUN+= part doesn't pass MAC address of the headset as argument to script, you just run the script alone. See here for an example: stackoverflow.com/a/14982520/3701431 Apr 30, 2017 at 17:27
  • 1
    Try export DISPLAY=:0 it may be not enough for pactl used by that script. Look at this post answers, they contain some interesting env variables askubuntu.com/q/458194/26246 . otherwise search for udev pactl
    – user.dz
    May 3, 2017 at 22:40
  • 1
    @hife, I don't think udev RUN support that (I didn't see before in udev doc neither I tried such thing, to be sure) . You may add ENV{DISPLAY}=":0" before RUN+= , ref: reactivated.net/writing_udev_rules.html#env , or use another shell script to wrap the target python script.
    – user.dz
    May 3, 2017 at 23:27
  • 1
    @user.dz I found out that pactl was more stable than pacmd and the final missing variable was XAUTHORITY (I just added all variables that printenv showed in a normal user terminal to a wrapper script, saw that it worked, then repeatedly removed half the variables until it broke again, etc.; In hindsight, I could have guessed it.) Thank you very much for your help.
    – hife
    May 7, 2017 at 18:03

2 Answers 2

5

My working solution

  1. Modify a2dp.py by replacing all instances of pacmd with pactl adjusting pacmd list-sinks to pactl list sinks (in my case saved as /usr/local/bin/a2dp_2.sh).

  2. Create a wrapper script /usr/local/bin/a2dp-wrapper.sh

    #!/bin/bash
    
    MAC=$1
    MACMOD=$(echo $MAC | sed 's/:/_/g')
    
    PID=$(pgrep pulseaudio)
    USER=$(grep -z USER= /proc/$PID/environ | sed 's/.*=//')
    
    export DISPLAY=:0
    export XAUTHORITY=/home/$USER/.Xauthority
    
    if pactl list sinks short | grep "bluez_sink\.$MACMOD.*SUSPENDED" 
        then
        sudo -u $USER /usr/local/bin/a2dp_2.py $MAC
    fi
    
  3. Add the following line to /etc/udev/rules.d/80-bt-headset.rules:

    ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/usr/local/bin/a2dp-wrapper.sh $attr{name}"
    

This wrapper script accomplishes the following:

  1. It finds out the $USER owning the running instance of pulseaudio, then sets the environment variables DISPLAY=:0 and XAUTHORITY=/home/$USER/.Xauthority necessary for pactl to work. This should make it work for all users on a machine. (I have not tested the effects of multiple users logged in at the same time.)

  2. It checks whether the corresponding sink is suspended and only then runs a2dp_2.py. This is necessary to prevent an infinite loop caused by a2dp_2.py reconnecting the device and thus triggering the rule.

  3. It runs a2dp_2.py as $USER. If run as root, a2dp_2.py will leave pulseaudio, and thus any audio settings, inaccessible without root privileges.

Alternatives: dbus loop/fixed package

  1. An alternative solution using a dbus loop can be found on the sript developer's page.

  2. A fix for the original bug is now available here and can be easily installed by adding ppa:ubuntu-audio-dev/pulse-testing and updating the available packages.

Hint: Finding your device's MAC address

Not strictly part of the original problem, but this might be useful for future reference. There are numerous ways to find your device's MAC address. The following is the one I consider most helpful for udev rules:

  1. Find the device path by running udevadm monitor and then connecting your device. Your output should look something like this:

    USER@MACHINE:~$ udevadm monitor
    monitor will print the received events for:
    UDEV - the event which udev sends out after rule processing
    KERNEL - the kernel uevent
    
    KERNEL[123043.617276] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
    UDEV  [123043.647291] add      /devices/pci0000:00/0000:00:14.0/usb3/3-7/3-7:1.0/bluetooth/hci0/hci0:256 (bluetooth)
    KERNEL[123044.153776] add      /devices/virtual/input/input68 (input)
    KERNEL[123044.153911] add      /devices/virtual/input/input68/event17 (input)
    UDEV  [123044.193415] add      /devices/virtual/input/input68 (input)
    UDEV  [123044.213213] add      /devices/virtual/input/input68/event17 (input)
    

    Stop the monitor with Ctrl+C. We have found three device paths. The one relevant for us is /devices/virtual/input/input68.

  2. Plug the obtained path into udevadm info:

    USER@MACHINE:~$ udevadm info -a -p /devices/virtual/input/input68
    
    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.
    
      looking at device '/devices/virtual/input/input68':
        KERNEL=="input68"
        SUBSYSTEM=="input"
        DRIVER==""
        ATTR{name}=="00:22:37:3D:DA:50"
        ATTR{phys}==""
        ATTR{properties}=="0"
        ATTR{uniq}==""
    

    We learn that the MAC address is 00:22:37:3D:DA:50 and also that it is stored as ATTR{name}.

Even if the output looks completely different, these two commands will be a good start in looking for the relevant conditions for a udev rule.

Experimental: Catching arbitrary bluetooth audio devices

The rule:

ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="??:??:??:??:??:??" RUN+="/usr/local/bin/a2dp-wrapper.sh $attr{name}"

will trigger for any input device that has a name attribute that looks like a MAC address and the conditional in the wrapper script should make sure no unintended actions are taken.

I do not have any other bluetooth audio device readily available to test this, but I see a number of potential issues:

  1. This will only work for a bluetooth device that is recognised as an input device containing the MAC address in the name attribute. Not every device may be recognised as such.

  2. This solution is not very elegant, as the rule will be triggered for any input device. However, I have not been able to find clear indicators to identify a bluetooth audio device. (As seen above, the input device has no further attributes, and the bluetooth device shows no indication of being an audio device, nor does it contain the MAC address. Maybe ACPI would be better for this.)

  3. You may not want to treat every bluetooth audio device the same: You may want to use the HSP protocol for your headset or you may not want to automatically switch to your housemate's speakers, that you've paired with at some point, whenever they are available. In those cases it is probably preferable to have a separate rule for each device.

I will keep updating this post as I learn more.

6
  • 1
    For multi-user setup this answer askubuntu.com/a/642898/26246 has nice idea for getting the required env var from a running process. That question was about media player, you may choose a base process that start for any user session. For inf loop, why does it reconnect? if to change driver or any attribute udev know about it, then use that attribute in udev matching condition. Whatever is the case, you may touch a file and use it as time stamp, your script should check that file age on the start if it is new (ex: <30sec) skip the script.
    – user.dz
    May 8, 2017 at 15:55
  • @user.dz Thank you very much once more. Thanks to your advice I was able to create a wrapper script that solves both remaining issues. As to why it reconnects and whether this is detectable by udev, I do not know yet. That will require further testing.
    – hife
    May 8, 2017 at 22:25
  • 2
    FYI, you can use RUN+="/usr/local/bin/a2dp-wrapper.sh $attr{name}" to pass the actual device name to the program if you want this to work for other device names. That would also the Don’t Repeat Yourself principle. May 10, 2017 at 13:00
  • @DavidFoerster Good point. Thank you. I replaced it.
    – hife
    May 10, 2017 at 18:08
  • 1
    Nice man, Interesting, My script is inspired many nerds to run it automatically. such a lazy people :)
    – pylover
    May 10, 2017 at 23:15
0

I found a (I think easier and also Wayland-compatible) way to do this with systemctl.

The udev rule would be the same as the OP's answer:

ACTION=="add", SUBSYSTEM=="input" ATTR{name}=="00:22:37:3D:DA:50" RUN+="/usr/local/bin/a2dp-wrapper.sh $attr{name}"

usr/local/bin/a2dp-wrapper.sh would be:

#!/bin/bash
MAC=$1
MACMOD=$(echo $MAC | sed 's/:/_/g')
PID=$(pgrep pulseaudio)
USER=$(grep -z USER= /proc/$PID/environ | sed 's/.*=//')

if pactl list sinks short | grep "bluez_sink\.$MACMOD.*SUSPENDED" 
    then
    systemctl [email protected] --user --now start /usr/local/bin/a2dp_2.py $MAC
fi

And the appropriate systemd service file in ~/.config/systemd/user/hdmi_sound_toggle.service is:

[Unit]
Description=Runs /usr/local/bin/a2dp_2.py
  
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hdmi_sound_toggle.py

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .