Skip to content

Instantly share code, notes, and snippets.

@ragecryx
Forked from pylover/a2dp.py
Created August 8, 2020 22:20
Show Gist options
  • Select an option

  • Save ragecryx/b5bb7a708d6772634f2c3d027a25a264 to your computer and use it in GitHub Desktop.

Select an option

Save ragecryx/b5bb7a708d6772634f2c3d027a25a264 to your computer and use it in GitHub Desktop.

Revisions

  1. @pylover pylover revised this gist Jul 18, 2017. 1 changed file with 5 additions and 2 deletions.
    7 changes: 5 additions & 2 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -35,6 +35,9 @@
    Change Log
    ----------
    - 0.5.2
    * Increasing the number of tries to 15.
    - 0.5.2
    * Optimizing waits.
    @@ -88,7 +91,7 @@
    import argparse


    __version__ = '0.5.1'
    __version__ = '0.5.2'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    @@ -97,7 +100,7 @@
    DEVICE_PATTERN = re.compile('^(?:.*\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:.*\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    WAIT_TIME = 2.25
    TRIES = 7
    TRIES = 15
    PROFILE = 'a2dp'


  2. @pylover pylover revised this gist Jun 26, 2017. 1 changed file with 6 additions and 2 deletions.
    8 changes: 6 additions & 2 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -35,6 +35,9 @@
    Change Log
    ----------
    - 0.5.2
    * Optimizing waits.
    - 0.5.1
    * Increasing WAIT_TIME and TRIES
    @@ -234,8 +237,8 @@ async def select_paired_device(self):
    return devices[0 if not selected.strip() else (int(selected) - 1)]


    async def wait():
    return await asyncio.sleep(WAIT_TIME)
    async def wait(delay=None):
    return await asyncio.sleep(WAIT_TIME or delay)


    async def execute_command(cmd, ignore_fail=False):
    @@ -362,6 +365,7 @@ async def main(args):
    device_id = await find_dev_id(mac)
    print('Device ID: %s' % device_id)

    await wait(2)
    await set_profile(device_id, args.profile)
    await set_default_sink(sink)
    await move_streams_to_sink(sink)
  3. @pylover pylover revised this gist Jun 25, 2017. 1 changed file with 6 additions and 3 deletions.
    9 changes: 6 additions & 3 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -35,6 +35,9 @@
    Change Log
    ----------
    - 0.5.1
    * Increasing WAIT_TIME and TRIES
    - 0.5.0
    * Autodetect & autorun service
    @@ -82,16 +85,16 @@
    import argparse


    __version__ = '0.5.0'
    __version__ = '0.5.1'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    HEX_BYTE_PATTERN = '%s{2}' % HEX_DIGIT_PATTERN
    MAC_ADDRESS_PATTERN = ':'.join((HEX_BYTE_PATTERN, ) * 6)
    DEVICE_PATTERN = re.compile('^(?:.*\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:.*\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    WAIT_TIME = .75
    TRIES = 4
    WAIT_TIME = 2.25
    TRIES = 7
    PROFILE = 'a2dp'


  4. @pylover pylover revised this gist Jun 9, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -82,7 +82,7 @@
    import argparse


    __version__ = '0.4.0'
    __version__ = '0.5.0'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
  5. @pylover pylover revised this gist Apr 19, 2017. 3 changed files with 71 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -25,14 +25,19 @@
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Thanks to:
    * https://github.com/DominicWatson, for adding the ``-p/--profile`` argument.
    * https://github.com/IzzySoft, for mentioning wait before connecting again.
    * https://github.com/AmploDev, for v0.4.0
    * https://github.com/Mihara, for autodetect & autorun service
    * https://github.com/dabrovnijk, for systemd service
    Change Log
    ----------
    - 0.5.0
    * Autodetect & autorun service
    - 0.4.1
    * Sorting device list
    54 changes: 54 additions & 0 deletions fix-bt.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,54 @@
    #!/usr/bin/python

    import dbus
    from dbus.mainloop.glib import DBusGMainLoop
    import gobject

    import subprocess
    import time

    # Where you keep the above script. Must be executable, obviously.
    A2DP = '/home/mihara/.local/bin/a2dp'

    # A list of device IDs that you wish to run it on.
    DEV_IDS = ['00:18:09:30:FC:D8','FC:58:FA:B1:2D:25']

    device_paths = []
    devices = []

    dbus_loop = DBusGMainLoop()
    bus = dbus.SystemBus(mainloop=dbus_loop)

    def generate_handler(device_id):

    last_ran = [0] # WHOA, this is crazy closure behavior: A simple variable does NOT work.

    def cb(*args, **kwargs):
    if args[0] == 'org.bluez.MediaControl1':
    if args[1].get('Connected'):
    print("Connected {}".format(device_id))
    if last_ran[0] < time.time() - 120:
    print("Fixing...")
    subprocess.call([A2DP,device_id])
    last_ran[0] = time.time()
    else:
    print("Disconnected {}".format(device_id))

    return cb

    # Figure out the path to the headset
    man = bus.get_object('org.bluez', '/')
    iface = dbus.Interface(man, 'org.freedesktop.DBus.ObjectManager')
    for device in iface.GetManagedObjects().keys():
    for id in DEV_IDS:
    if device.endswith(id.replace(':','_')):
    device_paths.append((id, device))

    for id, device in device_paths:
    headset = bus.get_object('org.bluez', device)
    headset.connect_to_signal("PropertiesChanged", generate_handler(id), dbus_interface='org.freedesktop.DBus.Properties')
    devices.append(headset)

    loop = gobject.MainLoop()
    loop.run()

    11 changes: 11 additions & 0 deletions fix-bt.service
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    [Unit]
    Description=Fix BT Speaker

    [Service]
    Type=simple
    Restart=always
    ExecStart=/usr/bin/fix-bt.py

    [Install]
    WantedBy=multi-user.target

  6. @pylover pylover revised this gist Apr 16, 2017. 1 changed file with 9 additions and 4 deletions.
    13 changes: 9 additions & 4 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -24,13 +24,18 @@
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Thank's to https://github.com/DominicWatson, for adding the ``-p/--profile`` argument.
    Thank's to https://github.com/IzzySoft, for mentioning wait before connecting again.
    Thank's to https://github.com/AmploDev, for v0.4.0
    Thanks to:
    * https://github.com/DominicWatson, for adding the ``-p/--profile`` argument.
    * https://github.com/IzzySoft, for mentioning wait before connecting again.
    * https://github.com/AmploDev, for v0.4.0
    Change Log
    ----------
    - 0.4.1
    * Sorting device list
    - 0.4.0
    * Adding ignore_fail argument by @AmploDev.
    * Sending all available streams into selected sink, after successfull connection by @AmploDev.
    @@ -191,7 +196,7 @@ async def get_list(self, command, pattern):
    m = pattern.match(l)
    if m:
    result.add(m.groups())
    return list(result)
    return sorted(list(result), key=lambda i: i[1])
    finally:
    self.not_listen_output()

  7. @pylover pylover revised this gist Apr 16, 2017. 1 changed file with 23 additions and 4 deletions.
    27 changes: 23 additions & 4 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -26,11 +26,15 @@
    Thank's to https://github.com/DominicWatson, for adding the ``-p/--profile`` argument.
    Thank's to https://github.com/IzzySoft, for mentioning wait before connecting again.
    Thank's to https://github.com/AmploDev, for v0.4.0
    Change Log
    ----------
    - 0.4.0
    * Adding ignore_fail argument by @AmploDev.
    * Sending all available streams into selected sink, after successfull connection by @AmploDev.
    - 0.3.3
    * Updating default sink before turning to ``off`` profile.
    @@ -68,7 +72,7 @@
    import argparse


    __version__ = '0.3.3'
    __version__ = '0.4.0'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    @@ -221,14 +225,18 @@ async def wait():
    return await asyncio.sleep(WAIT_TIME)


    async def execute_command(cmd):
    async def execute_command(cmd, ignore_fail=False):
    p = await asyncio.create_subprocess_shell(cmd, stdout=sb.PIPE, stderr=sb.PIPE)
    stdout, stderr = await p.communicate()
    stdout, stderr = \
    stdout.decode() if stdout is not None else '', \
    stderr.decode() if stderr is not None else ''
    if p.returncode != 0 or stderr.strip() != '':
    raise SubprocessError('Command: %s failed with status: %s\nstderr: %s' % (cmd, p.returncode, stderr))
    message = 'Command: %s failed with status: %s\nstderr: %s' % (cmd, p.returncode, stderr)
    if ignore_fail:
    print('Ignoring: %s' % message)
    else:
    raise SubprocessError(message)
    return stdout


    @@ -277,6 +285,16 @@ async def set_default_sink(sink):
    return await execute_command('pacmd set-default-sink %s' % sink)


    async def move_streams_to_sink(sink):
    streams = await execute_command('pacmd list-sink-inputs | grep "index:"', True)
    for i in streams.split():
    i = ''.join(n for n in i if n.isdigit())
    if i != '':
    print('Moving stream %s to sink' % i)
    await execute_command('pacmd move-sink-input %s %s' % (i, sink))
    return sink


    async def main(args):
    global WAIT_TIME, TRIES

    @@ -333,6 +351,7 @@ async def main(args):

    await set_profile(device_id, args.profile)
    await set_default_sink(sink)
    await move_streams_to_sink(sink)

    except (SubprocessError, RetryExceededError) as ex:
    print(str(ex), file=sys.stderr)
  8. @pylover pylover revised this gist Mar 24, 2017. 1 changed file with 13 additions and 9 deletions.
    22 changes: 13 additions & 9 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -31,6 +31,9 @@
    Change Log
    ----------
    - 0.3.3
    * Updating default sink before turning to ``off`` profile.
    - 0.3.2
    * Waiting a bit: ``-w/--wait`` before connecting again.
    @@ -65,7 +68,7 @@
    import argparse


    __version__ = '0.3.2'
    __version__ = '0.3.3'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    @@ -161,9 +164,11 @@ async def send_and_wait(self, cmd, wait_expression, fail_expression='fail'):
    self.not_listen_output()

    async def disconnect(self, mac):
    print('Disconnecting the device.')
    await self.send_and_wait('disconnect %s' % ':'.join(mac), 'Successful disconnected')

    async def connect(self, mac):
    print('Connecting again.')
    await self.send_and_wait('connect %s' % ':'.join(mac), 'Connection successful')

    async def trust(self, mac):
    @@ -196,6 +201,7 @@ async def list_controllers(self):
    return await self.get_list('list', CONTROLLER_PATTERN)

    async def select_paired_device(self):
    print('Selecting device:')
    devices = await self.list_paired_devices()
    count = len(devices)

    @@ -258,6 +264,7 @@ async def find_sink(mac, **kw):


    async def set_profile(device_id, profile):
    print('Setting the %s profile' % profile)
    try:
    return await execute_command('pactl set-card-profile %s %s' % (device_id, _profiles[profile]))
    except KeyError:
    @@ -266,6 +273,7 @@ async def set_profile(device_id, profile):


    async def set_default_sink(sink):
    print('Updating default sink to %s' % sink)
    return await execute_command('pacmd set-default-sink %s' % sink)


    @@ -290,7 +298,6 @@ async def main(args):
    try:

    if mac is None:
    print('Selecting device:')
    mac, _ = await protocol.select_paired_device()

    mac = mac.split(':' if ':' in mac else '_')
    @@ -311,23 +318,20 @@ async def main(args):
    print('Device ID: %s' % device_id)
    print('Sink: %s' % sink)

    print('Turning off audio profile.')
    await set_profile(device_id, profile='off')
    await set_default_sink(sink)
    await wait()

    await set_profile(device_id, 'off')

    if args.profile is 'a2dp':
    print('Disconnecting the device.')
    await protocol.disconnect(mac)

    print('Connecting again.')
    await wait()
    await protocol.connect(mac)

    device_id = await find_dev_id(mac)
    print('Device ID: %s' % device_id)

    print('Setting the %s profile' % args.profile)
    await set_profile(device_id, args.profile)
    print('Updating default sink')
    await set_default_sink(sink)

    except (SubprocessError, RetryExceededError) as ex:
  9. @pylover pylover revised this gist Mar 9, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -65,7 +65,7 @@
    import argparse


    __version__ = '0.3.1'
    __version__ = '0.3.2'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
  10. @pylover pylover revised this gist Mar 9, 2017. 1 changed file with 22 additions and 13 deletions.
    35 changes: 22 additions & 13 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -16,7 +16,7 @@
    $ alias speakers="a2dp.py 10:08:C1:44:AE:BC"
    $ alias headphones="a2dp.py 00:22:37:3D:DA:50"
    $ alias headset="a2dp.py 00:22:37:F8:A0:77 -p headset_head_unit"
    $ alias headset="a2dp.py 00:22:37:F8:A0:77 -p hsp"
    $ speakers
    @@ -31,7 +31,7 @@
    Change Log
    ----------
    - 0.3.1
    - 0.3.2
    * Waiting a bit: ``-w/--wait`` before connecting again.
    - 0.3.0
    @@ -75,9 +75,15 @@
    CONTROLLER_PATTERN = re.compile('^(?:.*\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    WAIT_TIME = .75
    TRIES = 4
    PROFILE = 'a2dp_sink'
    PROFILE = 'a2dp'


    _profiles = {
    'a2dp': 'a2dp_sink',
    'hsp': 'headset_head_unit',
    'off': 'off'
    }

    # CLI Arguments
    parser = argparse.ArgumentParser(prog=sys.argv[0])
    parser.add_argument('-e', '--echo', action='store_true', default=False,
    @@ -87,7 +93,7 @@
    parser.add_argument('-t', '--tries', default=TRIES, type=int,
    help='The number of tries if subprocess is failed. default is: %s' % TRIES)
    parser.add_argument('-p', '--profile', default=PROFILE,
    help='The profile to switch to. default is: %s' % PROFILE)
    help='The profile to switch to. available options are: hsp, a2dp. default is: %s' % PROFILE)
    parser.add_argument('-V', '--version', action='store_true', help='Show the version.')
    parser.add_argument('mac', nargs='?', default=None)

    @@ -252,15 +258,19 @@ async def find_sink(mac, **kw):


    async def set_profile(device_id, profile):
    return await execute_command('pactl set-card-profile %s %s' % (device_id, profile))
    try:
    return await execute_command('pactl set-card-profile %s %s' % (device_id, _profiles[profile]))
    except KeyError:
    print('Invalid profile: %s, please select one one of a2dp or hsp.' % profile, file=sys.stderr)
    raise SystemExit(1)


    async def set_default_sink(sink):
    return await execute_command('pacmd set-default-sink %s' % sink)


    async def main(args):
    global WAIT_TIME, TRIES, PROFILE
    global WAIT_TIME, TRIES

    if args.version:
    print(__version__)
    @@ -271,7 +281,6 @@ async def main(args):
    # Hacking, Changing the constants!
    WAIT_TIME = args.wait
    TRIES = args.tries
    PROFILE = args.profile

    exit_future = asyncio.Future()
    transport, protocol = await asyncio.get_event_loop().subprocess_exec(
    @@ -296,7 +305,7 @@ async def main(args):

    sink = await find_sink(mac, fail_safe=True)
    if sink is None:
    await set_profile(device_id, PROFILE)
    await set_profile(device_id, args.profile)
    sink = await find_sink(mac)

    print('Device ID: %s' % device_id)
    @@ -305,19 +314,19 @@ async def main(args):
    print('Turning off audio profile.')
    await set_profile(device_id, profile='off')

    if PROFILE is 'a2dp_sink':
    if args.profile is 'a2dp':
    print('Disconnecting the device.')
    await protocol.disconnect(mac)

    print('Connecting again.')
    await wait()
    await protocol.connect(mac)

    print('Setting profile')
    device_id = await find_dev_id(mac)
    print('Device ID: %s' % device_id)

    await set_profile(device_id, PROFILE)
    print('Setting the %s profile' % args.profile)
    await set_profile(device_id, args.profile)
    print('Updating default sink')
    await set_default_sink(sink)

    @@ -332,8 +341,8 @@ async def main(args):
    # Close the stdout pipe
    transport.close()

    if PROFILE is 'a2dp_sink':
    print('Enjoy HiFi stereo music!')
    if args.profile == 'a2dp':
    print('"Enjoy" the HiFi stereo music :)')
    else:
    print('"Enjoy" your headset audio :)')

  11. @pylover pylover revised this gist Mar 9, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -65,7 +65,7 @@
    import argparse


    __version__ = '0.3.0'
    __version__ = '0.3.1'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
  12. @pylover pylover revised this gist Mar 9, 2017. 1 changed file with 10 additions and 2 deletions.
    12 changes: 10 additions & 2 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -6,14 +6,17 @@
    Workaround for bug: https://bugs.launchpad.net/ubuntu/+source/indicator-sound/+bug/1577197
    Run it with python3.5 or higher after pairing/connecting the bluetooth stereo headphone.
    Only works with bluez5.
    This will be only fixes the bluez5 problem mentioned above .
    Licence: Freeware
    See ``python3.5 a2dp.py -h``.
    Shorthands:
    $ alias speakers="a2dp.py 10:08:C1:44:AE:BC"
    $ alias headphones="a2dp.py 00:22:37:3D:DA:50"
    $ alias headset="a2dp.py 00:22:37:F8:A0:77 -p headset_head_unit"
    $ speakers
    @@ -22,11 +25,15 @@
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Thank's to https://github.com/DominicWatson, for adding the ``-p/--profile`` argument.
    Licence: Freeware
    Thank's to https://github.com/IzzySoft, for mentioning wait before connecting again.
    Change Log
    ----------
    - 0.3.1
    * Waiting a bit: ``-w/--wait`` before connecting again.
    - 0.3.0
    * Adding -p / --profile option for using the same script to switch between headset and A2DP audio profiles
    @@ -303,6 +310,7 @@ async def main(args):
    await protocol.disconnect(mac)

    print('Connecting again.')
    await wait()
    await protocol.connect(mac)

    print('Setting profile')
  13. @pylover pylover revised this gist Mar 9, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -58,7 +58,7 @@
    import argparse


    __version__ = '0.2.5'
    __version__ = '0.3.0'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
  14. @pylover pylover revised this gist Mar 9, 2017. 1 changed file with 28 additions and 28 deletions.
    56 changes: 28 additions & 28 deletions a2dp.py
    100644 → 100755
    Original file line number Diff line number Diff line change
    @@ -1,20 +1,5 @@
    #! /usr/bin/env python3.5
    """
    ####################################################################
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    Version 2, December 2004
    Copyright (C) 2016 Vahid Mardani
    Everyone is permitted to copy and distribute verbatim or modified
    copies of this license document, and changing it is allowed as long
    as the name is changed.
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
    0. You just DO WHAT THE FUCK YOU WANT TO.
    ####################################################################
    Fixing bluetooth stereo headphone/headset problem in ubuntu 16.04 and also debian jessie, with bluez5.
    @@ -23,21 +8,28 @@
    Only works with bluez5.
    See `python3.5 a2dp.py -h`.
    See ``python3.5 a2dp.py -h``.
    Shorthands:
    $ alias speakers="a2dp.py 10:08:C1:44:AE:BC"
    $ alias headphones="a2dp.py 00:22:37:3D:DA:50"
    $ speakers
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Thank's to https://github.com/DominicWatson, for adding the ``-p/--profile`` argument.
    Licence: Freeware
    Change Log
    ----------
    - 0.3.0
    * Adding -p / --profile option for using the same script to switch between headset and A2DP audio profiles
    - 0.2.5
    * Mentioning [mac] argument.
    @@ -75,7 +67,8 @@
    DEVICE_PATTERN = re.compile('^(?:.*\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:.*\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    WAIT_TIME = .75
    TRIES = 8
    TRIES = 4
    PROFILE = 'a2dp_sink'


    # CLI Arguments
    @@ -86,6 +79,8 @@
    help='The seconds to wait for subprocess output, default is: %s' % WAIT_TIME)
    parser.add_argument('-t', '--tries', default=TRIES, type=int,
    help='The number of tries if subprocess is failed. default is: %s' % TRIES)
    parser.add_argument('-p', '--profile', default=PROFILE,
    help='The profile to switch to. default is: %s' % PROFILE)
    parser.add_argument('-V', '--version', action='store_true', help='Show the version.')
    parser.add_argument('mac', nargs='?', default=None)

    @@ -249,7 +244,7 @@ async def find_sink(mac, **kw):
    return await execute_find('pacmd list-sinks', 'bluez_sink.%s' % '_'.join(mac), **kw)


    async def set_profile(device_id, profile='a2dp_sink'):
    async def set_profile(device_id, profile):
    return await execute_command('pactl set-card-profile %s %s' % (device_id, profile))


    @@ -258,7 +253,7 @@ async def set_default_sink(sink):


    async def main(args):
    global WAIT_TIME, TRIES
    global WAIT_TIME, TRIES, PROFILE

    if args.version:
    print(__version__)
    @@ -269,6 +264,7 @@ async def main(args):
    # Hacking, Changing the constants!
    WAIT_TIME = args.wait
    TRIES = args.tries
    PROFILE = args.profile

    exit_future = asyncio.Future()
    transport, protocol = await asyncio.get_event_loop().subprocess_exec(
    @@ -293,7 +289,7 @@ async def main(args):

    sink = await find_sink(mac, fail_safe=True)
    if sink is None:
    await set_profile(device_id)
    await set_profile(device_id, PROFILE)
    sink = await find_sink(mac)

    print('Device ID: %s' % device_id)
    @@ -302,17 +298,18 @@ async def main(args):
    print('Turning off audio profile.')
    await set_profile(device_id, profile='off')

    print('Disconnecting the device.')
    await protocol.disconnect(mac)
    if PROFILE is 'a2dp_sink':
    print('Disconnecting the device.')
    await protocol.disconnect(mac)

    print('Connecting again.')
    await protocol.connect(mac)
    print('Connecting again.')
    await protocol.connect(mac)

    print('Setting A2DP profile')
    print('Setting profile')
    device_id = await find_dev_id(mac)
    print('Device ID: %s' % device_id)
    await set_profile(device_id)

    await set_profile(device_id, PROFILE)
    print('Updating default sink')
    await set_default_sink(sink)

    @@ -327,7 +324,10 @@ async def main(args):
    # Close the stdout pipe
    transport.close()

    print('Enjoy HiFi stereo music!')
    if PROFILE is 'a2dp_sink':
    print('Enjoy HiFi stereo music!')
    else:
    print('"Enjoy" your headset audio :)')


    if __name__ == '__main__':
  15. @pylover pylover revised this gist Nov 20, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -28,7 +28,7 @@
    Shorthands:
    $ alias speakers="a2dp.py 10:08:C1:44:AE:BC"
    $ alias heaphones="a2dp.py 00:22:37:3D:DA:50"
    $ alias headphones="a2dp.py 00:22:37:3D:DA:50"
    $ speakers
  16. @pylover pylover revised this gist Nov 20, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -30,7 +30,7 @@
    $ alias speakers="a2dp.py 10:08:C1:44:AE:BC"
    $ alias heaphones="a2dp.py 00:22:37:3D:DA:50"
    $ ./speakers
    $ speakers
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
  17. @pylover pylover revised this gist Nov 20, 2016. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion a2dp.py
    Original file line number Diff line number Diff line change
    @@ -38,6 +38,9 @@
    Change Log
    ----------
    - 0.2.5
    * Mentioning [mac] argument.
    - 0.2.4
    * Removing duplicated devices in select device list.
    @@ -63,7 +66,7 @@
    import argparse


    __version__ = '0.2.4'
    __version__ = '0.2.5'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
  18. @pylover pylover revised this gist Nov 20, 2016. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -25,6 +25,14 @@
    See `python3.5 a2dp.py -h`.
    Shorthands:
    $ alias speakers="a2dp.py 10:08:C1:44:AE:BC"
    $ alias heaphones="a2dp.py 00:22:37:3D:DA:50"
    $ ./speakers
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Change Log
  19. @pylover pylover revised this gist Oct 24, 2016. 1 changed file with 7 additions and 4 deletions.
    11 changes: 7 additions & 4 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -30,6 +30,9 @@
    Change Log
    ----------
    - 0.2.4
    * Removing duplicated devices in select device list.
    - 0.2.3
    * Matching ANSI escape characters. Tested on 16.10 & 16.04
    @@ -52,7 +55,7 @@
    import argparse


    __version__ = '0.2.3'
    __version__ = '0.2.4'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    @@ -151,16 +154,16 @@ async def quit(self):
    await self.send_command('quit')

    async def get_list(self, command, pattern):
    result = []
    result = set()
    try:
    self.listen_output()
    await self.send_command(command)
    await wait()
    for l in self.output.splitlines():
    m = pattern.match(l)
    if m:
    result.append(m.groups())
    return result
    result.add(m.groups())
    return list(result)
    finally:
    self.not_listen_output()

  20. @pylover pylover revised this gist Oct 23, 2016. 1 changed file with 6 additions and 3 deletions.
    9 changes: 6 additions & 3 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -30,6 +30,9 @@
    Change Log
    ----------
    - 0.2.3
    * Matching ANSI escape characters. Tested on 16.10 & 16.04
    - 0.2.2
    * Some sort of code enhancements.
    @@ -49,14 +52,14 @@
    import argparse


    __version__ = '0.2.2'
    __version__ = '0.2.3'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    HEX_BYTE_PATTERN = '%s{2}' % HEX_DIGIT_PATTERN
    MAC_ADDRESS_PATTERN = ':'.join((HEX_BYTE_PATTERN, ) * 6)
    DEVICE_PATTERN = re.compile('^(?:\[NEW\]\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:\[NEW\]\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    DEVICE_PATTERN = re.compile('^(?:.*\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:.*\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    WAIT_TIME = .75
    TRIES = 8

  21. @pylover pylover revised this gist Oct 21, 2016. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion .gitignore
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    .idea
  22. @pylover pylover revised this gist Oct 21, 2016. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions .gitignore
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    .idea
  23. @pylover pylover revised this gist Oct 21, 2016. 1 changed file with 32 additions and 46 deletions.
    78 changes: 32 additions & 46 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,7 @@
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    Version 2, December 2004
    Copyright (C) 2004 Sam Hocevar <[email protected]>
    Copyright (C) 2016 Vahid Mardani
    Everyone is permitted to copy and distribute verbatim or modified
    copies of this license document, and changing it is allowed as long
    @@ -27,12 +27,10 @@
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Vahid Mardani
    Change Log
    ----------
    - 0.2.1
    - 0.2.2
    * Some sort of code enhancements.
    - 0.2.0
    @@ -51,16 +49,16 @@
    import argparse


    __version__ = '0.2.1'
    __version__ = '0.2.2'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    HEX_BYTE_PATTERN = '%s{2}' % HEX_DIGIT_PATTERN
    MAC_ADDRESS_PATTERN = ':'.join((HEX_BYTE_PATTERN, ) * 6)
    DEVICE_PATTERN = re.compile('^(?:\[NEW\]\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:\[NEW\]\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    WAIT_TIME = 1
    TRIES = 12
    WAIT_TIME = .75
    TRIES = 8


    # CLI Arguments
    @@ -149,36 +147,31 @@ async def trust(self, mac):
    async def quit(self):
    await self.send_command('quit')

    async def list_devices(self):
    async def get_list(self, command, pattern):
    result = []
    try:
    self.listen_output()
    await self.send_command('devices')
    await self.send_command(command)
    await wait()
    for l in self.output.splitlines():
    m = DEVICE_PATTERN.match(l)
    m = pattern.match(l)
    if m:
    result.append(m.groups())
    return result
    finally:
    self.not_listen_output()

    async def list_devices(self):
    return await self.get_list('devices', DEVICE_PATTERN)

    async def list_paired_devices(self):
    return await self.get_list('paired-devices', DEVICE_PATTERN)

    async def list_controllers(self):
    result = []
    try:
    self.listen_output()
    await self.send_command('list')
    await wait()
    for l in self.output.splitlines():
    m = CONTROLLER_PATTERN.match(l)
    if m:
    result.append(m.groups())
    return result
    finally:
    self.not_listen_output()
    return await self.get_list('list', CONTROLLER_PATTERN)

    async def select_device(self):
    devices = await self.list_devices()
    async def select_paired_device(self):
    devices = await self.list_paired_devices()
    count = len(devices)

    if count < 1:
    @@ -190,7 +183,7 @@ async def select_device(self):
    print('%d. %s %s' % (i+1, d[0], d[1]))
    print('Select device[1]:')
    selected = input()
    return devices[int(selected) - 1]
    return devices[0 if not selected.strip() else (int(selected) - 1)]


    async def wait():
    @@ -208,10 +201,11 @@ async def execute_command(cmd):
    return stdout


    async def execute_find(cmd, pattern, tries=0, message=''):
    async def execute_find(cmd, pattern, tries=0, fail_safe=False):
    tries = tries or TRIES

    retry_message = ', Retrying %d more times'
    message = 'Cannot find `%s` using `%s`.' % (pattern, cmd)
    retry_message = message + ' Retrying %d more times'
    while True:
    stdout = await execute_command(cmd)
    match = re.search(pattern, stdout)
    @@ -220,29 +214,22 @@ async def execute_find(cmd, pattern, tries=0, message=''):
    return match.group()
    elif tries > 0:
    await wait()
    print(message + retry_message % tries)
    print(retry_message % tries)
    tries -= 1
    continue

    if fail_safe:
    return None

    raise RetryExceededError('Retry times exceeded: %s' % message)


    async def find_dev_id(mac, **kw):
    return await execute_find(
    'pactl list cards short',
    'bluez_card.%s' % '_'.join(mac),
    message='Cannot list cards using `pactl`',
    **kw
    )
    return await execute_find('pactl list cards short', 'bluez_card.%s' % '_'.join(mac), **kw)


    async def find_sink(mac, **kw):
    return await execute_find(
    'pacmd list-sinks',
    'bluez_sink.%s' % '_'.join(mac),
    message='Cannot list sinks using `pacmd`',
    **kw
    )
    return await execute_find('pacmd list-sinks', 'bluez_sink.%s' % '_'.join(mac), **kw)


    async def set_profile(device_id, profile='a2dp_sink'):
    @@ -275,20 +262,19 @@ async def main(args):

    if mac is None:
    print('Selecting device:')
    mac, _ = await protocol.select_device()
    mac, _ = await protocol.select_paired_device()

    mac = mac.split(':' if ':' in mac else '_')
    print('Device MAC: %s' % ':'.join(mac))

    device_id = await find_dev_id(mac)

    device_id = await find_dev_id(mac, fail_safe=True)
    if device_id is None:
    print('It seems device: %s is not connected yet, trying to connect.' % ':'.join(mac))
    await protocol.trust(mac)
    await protocol.connect(mac)
    device_id = await find_dev_id(mac)

    device_id = await find_dev_id(mac, tries=1)

    sink = await find_sink(mac)
    sink = await find_sink(mac, fail_safe=True)
    if sink is None:
    await set_profile(device_id)
    sink = await find_sink(mac)
  24. @pylover pylover revised this gist Oct 21, 2016. 1 changed file with 25 additions and 23 deletions.
    48 changes: 25 additions & 23 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -1,17 +1,17 @@
    #! /usr/bin/env python3.5
    """
    ####################################################################
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    Version 2, December 2004
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    Version 2, December 2004
    Copyright (C) 2004 Sam Hocevar <[email protected]>
    Copyright (C) 2004 Sam Hocevar <[email protected]>
    Everyone is permitted to copy and distribute verbatim or modified
    copies of this license document, and changing it is allowed as long
    as the name is changed.
    Everyone is permitted to copy and distribute verbatim or modified
    copies of this license document, and changing it is allowed as long
    as the name is changed.
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
    0. You just DO WHAT THE FUCK YOU WANT TO.
    ####################################################################
    @@ -32,6 +32,9 @@
    Change Log
    ----------
    - 0.2.1
    * Some sort of code enhancements.
    - 0.2.0
    * Adding `-V/--version`, `-w/--wait` and `-t/--tries` CLI arguments.
    @@ -48,7 +51,7 @@
    import argparse


    __version__ = '0.2.0'
    __version__ = '0.2.1'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    @@ -250,11 +253,21 @@ async def set_default_sink(sink):
    return await execute_command('pacmd set-default-sink %s' % sink)


    async def main(args, loop):
    async def main(args):
    global WAIT_TIME, TRIES

    if args.version:
    print(__version__)
    return 0

    mac = args.mac

    # Hacking, Changing the constants!
    WAIT_TIME = args.wait
    TRIES = args.tries

    exit_future = asyncio.Future()
    transport, protocol = await loop.subprocess_exec(
    transport, protocol = await asyncio.get_event_loop().subprocess_exec(
    lambda: BluetoothctlProtocol(exit_future, echo=args.echo), 'bluetoothctl'
    )

    @@ -315,15 +328,4 @@ async def main(args, loop):


    if __name__ == '__main__':
    arguments = parser.parse_args()

    if arguments.version:
    print(__version__)
    sys.exit(0)

    # Hacking, Changing the constants!
    WAIT_TIME = arguments.wait
    TRIES = arguments.tries

    main_loop = asyncio.get_event_loop()
    sys.exit(main_loop.run_until_complete(main(arguments, main_loop)))
    sys.exit(asyncio.get_event_loop().run_until_complete(main(parser.parse_args())))
  25. @pylover pylover revised this gist Oct 21, 2016. 1 changed file with 15 additions and 0 deletions.
    15 changes: 15 additions & 0 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,20 @@
    #! /usr/bin/env python3.5
    """
    ####################################################################
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    Version 2, December 2004
    Copyright (C) 2004 Sam Hocevar <[email protected]>
    Everyone is permitted to copy and distribute verbatim or modified
    copies of this license document, and changing it is allowed as long
    as the name is changed.
    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
    0. You just DO WHAT THE FUCK YOU WANT TO.
    ####################################################################
    Fixing bluetooth stereo headphone/headset problem in ubuntu 16.04 and also debian jessie, with bluez5.
  26. @pylover pylover revised this gist Oct 21, 2016. 1 changed file with 51 additions and 24 deletions.
    75 changes: 51 additions & 24 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -8,7 +8,7 @@
    Only works with bluez5.
    See `a2dp.py -h` for info.
    See `python3.5 a2dp.py -h`.
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    @@ -17,10 +17,12 @@
    Change Log
    ----------
    **0.1.1**
    - 0.2.0
    * Adding `-V/--version`, `-w/--wait` and `-t/--tries` CLI arguments.
    * Supporting the `[NEW]` prefix for devices & controllers as advised by @wdullaer
    * Drying the code.
    - 0.1.1
    * Supporting the `[NEW]` prefix for devices & controllers as advised by @wdullaer
    * Drying the code.
    """

    @@ -31,18 +33,31 @@
    import argparse


    __version__ = '0.1.1'
    __version__ = '0.2.0'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    HEX_BYTE_PATTERN = '%s{2}' % HEX_DIGIT_PATTERN
    MAC_ADDRESS_PATTERN = ':'.join((HEX_BYTE_PATTERN, ) * 6)
    DEVICE_PATTERN = re.compile('^(?:\[NEW\]\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:\[NEW\]\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    LONG_SLEEP = 1
    SHORT_SLEEP = .5
    WAIT_TIME = 1
    TRIES = 12


    # CLI Arguments
    parser = argparse.ArgumentParser(prog=sys.argv[0])
    parser.add_argument('-e', '--echo', action='store_true', default=False,
    help='If given, the subprocess stdout will be also printed on stdout.')
    parser.add_argument('-w', '--wait', default=WAIT_TIME, type=float,
    help='The seconds to wait for subprocess output, default is: %s' % WAIT_TIME)
    parser.add_argument('-t', '--tries', default=TRIES, type=int,
    help='The number of tries if subprocess is failed. default is: %s' % TRIES)
    parser.add_argument('-V', '--version', action='store_true', help='Show the version.')
    parser.add_argument('mac', nargs='?', default=None)


    # Exceptions
    class SubprocessError(Exception):
    pass

    @@ -100,7 +115,7 @@ async def send_and_wait(self, cmd, wait_expression, fail_expression='fail'):
    self.listen_output()
    await self.send_command(cmd)
    while not await self.search_in_output(wait_expression.lower(), fail_expression=fail_expression):
    await asyncio.sleep(SHORT_SLEEP)
    await wait()
    finally:
    self.not_listen_output()

    @@ -121,7 +136,7 @@ async def list_devices(self):
    try:
    self.listen_output()
    await self.send_command('devices')
    await asyncio.sleep(LONG_SLEEP)
    await wait()
    for l in self.output.splitlines():
    m = DEVICE_PATTERN.match(l)
    if m:
    @@ -135,7 +150,7 @@ async def list_controllers(self):
    try:
    self.listen_output()
    await self.send_command('list')
    await asyncio.sleep(LONG_SLEEP)
    await wait()
    for l in self.output.splitlines():
    m = CONTROLLER_PATTERN.match(l)
    if m:
    @@ -160,6 +175,10 @@ async def select_device(self):
    return devices[int(selected) - 1]


    async def wait():
    return await asyncio.sleep(WAIT_TIME)


    async def execute_command(cmd):
    p = await asyncio.create_subprocess_shell(cmd, stdout=sb.PIPE, stderr=sb.PIPE)
    stdout, stderr = await p.communicate()
    @@ -171,7 +190,8 @@ async def execute_command(cmd):
    return stdout


    async def execute_find(cmd, pattern, tries=1, message=''):
    async def execute_find(cmd, pattern, tries=0, message=''):
    tries = tries or TRIES

    retry_message = ', Retrying %d more times'
    while True:
    @@ -181,29 +201,29 @@ async def execute_find(cmd, pattern, tries=1, message=''):
    if match:
    return match.group()
    elif tries > 0:
    await asyncio.sleep(LONG_SLEEP)
    await wait()
    print(message + retry_message % tries)
    tries -= 1
    continue

    raise RetryExceededError('Retry times exceeded: %s' % message)


    async def find_dev_id(mac, tries=12):
    async def find_dev_id(mac, **kw):
    return await execute_find(
    'pactl list cards short',
    'bluez_card.%s' % '_'.join(mac),
    tries=tries,
    message='Cannot list cards using `pactl`'
    message='Cannot list cards using `pactl`',
    **kw
    )


    async def find_sink(mac, tries=12):
    async def find_sink(mac, **kw):
    return await execute_find(
    'pacmd list-sinks',
    'bluez_sink.%s' % '_'.join(mac),
    tries=tries,
    message='Cannot list sinks using `pacmd`'
    message='Cannot list sinks using `pacmd`',
    **kw
    )


    @@ -232,13 +252,13 @@ async def main(args, loop):
    mac = mac.split(':' if ':' in mac else '_')
    print('Device MAC: %s' % ':'.join(mac))

    device_id = await find_dev_id(mac, tries=5)
    device_id = await find_dev_id(mac)

    if device_id is None:
    await protocol.trust(mac)
    await protocol.connect(mac)

    device_id = await find_dev_id(mac)
    device_id = await find_dev_id(mac, tries=1)

    sink = await find_sink(mac)
    if sink is None:
    @@ -280,8 +300,15 @@ async def main(args, loop):


    if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog=sys.argv[0])
    parser.add_argument('-e', '--echo', action='store_true', default=False)
    parser.add_argument('mac', nargs='?', default=None)
    arguments = parser.parse_args()

    if arguments.version:
    print(__version__)
    sys.exit(0)

    # Hacking, Changing the constants!
    WAIT_TIME = arguments.wait
    TRIES = arguments.tries

    main_loop = asyncio.get_event_loop()
    sys.exit(main_loop.run_until_complete(main(parser.parse_args(), main_loop)))
    sys.exit(main_loop.run_until_complete(main(arguments, main_loop)))
  27. @pylover pylover revised this gist Oct 20, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,8 @@
    See `a2dp.py -h` for info.
    https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Vahid Mardani
    Change Log
    @@ -21,7 +22,6 @@
    * Supporting the `[NEW]` prefix for devices & controllers as advised by @wdullaer
    * Drying the code.
    """

    import sys
  28. @pylover pylover revised this gist Oct 20, 2016. 1 changed file with 66 additions and 49 deletions.
    115 changes: 66 additions & 49 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -12,9 +12,17 @@
    https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
    Vahid Mardani
    """
    __version__ = '0.1.0'
    Change Log
    ----------
    **0.1.1**
    * Supporting the `[NEW]` prefix for devices & controllers as advised by @wdullaer
    * Drying the code.
    """

    import sys
    import re
    @@ -23,14 +31,26 @@
    import argparse


    HEX_PATTERN = '[0-9A-F]'
    BYTE_HEX_PATTERN = '%s{2}' % HEX_PATTERN
    __version__ = '0.1.1'


    HEX_DIGIT_PATTERN = '[0-9A-F]'
    HEX_BYTE_PATTERN = '%s{2}' % HEX_DIGIT_PATTERN
    MAC_ADDRESS_PATTERN = ':'.join((HEX_BYTE_PATTERN, ) * 6)
    DEVICE_PATTERN = re.compile('^(?:\[NEW\]\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    CONTROLLER_PATTERN = re.compile('^(?:\[NEW\]\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
    LONG_SLEEP = 1
    SHORT_SLEEP = .5


    class SubprocessError(Exception):
    pass


    class RetryExceededError(Exception):
    pass


    class BluetoothctlProtocol(asyncio.SubprocessProtocol):
    def __init__(self, exit_future, echo=True):
    self.exit_future = exit_future
    @@ -61,6 +81,7 @@ def connection_made(self, transport):

    async def send_command(self, c):
    stdin_transport = self.transport.get_pipe_transport(0)
    # noinspection PyProtectedMember
    stdin_transport._pipe.write(('%s\n' % c).encode())

    async def search_in_output(self, expression, fail_expression=None):
    @@ -79,7 +100,7 @@ async def send_and_wait(self, cmd, wait_expression, fail_expression='fail'):
    self.listen_output()
    await self.send_command(cmd)
    while not await self.search_in_output(wait_expression.lower(), fail_expression=fail_expression):
    await asyncio.sleep(.3)
    await asyncio.sleep(SHORT_SLEEP)
    finally:
    self.not_listen_output()

    @@ -100,9 +121,9 @@ async def list_devices(self):
    try:
    self.listen_output()
    await self.send_command('devices')
    await asyncio.sleep(1)
    await asyncio.sleep(LONG_SLEEP)
    for l in self.output.splitlines():
    m = re.match('^Device\s(?P<mac>[:A-F0-9]{17})\s(?P<name>.*)', l)
    m = DEVICE_PATTERN.match(l)
    if m:
    result.append(m.groups())
    return result
    @@ -114,9 +135,9 @@ async def list_controllers(self):
    try:
    self.listen_output()
    await self.send_command('list')
    await asyncio.sleep(1)
    await asyncio.sleep(LONG_SLEEP)
    for l in self.output.splitlines():
    m = re.match('^Controller\s(?P<mac>[:A-F0-9]{17})\s(?P<name>.*)', l)
    m = CONTROLLER_PATTERN.match(l)
    if m:
    result.append(m.groups())
    return result
    @@ -150,43 +171,40 @@ async def execute_command(cmd):
    return stdout


    async def execute_find(cmd, pattern):
    stdout = await execute_command(cmd)
    match = re.search(pattern, stdout)
    if match:
    return match.group()
    return match
    async def execute_find(cmd, pattern, tries=1, message=''):

    retry_message = ', Retrying %d more times'
    while True:
    stdout = await execute_command(cmd)
    match = re.search(pattern, stdout)

    async def find_dev_id(mac, tries=12):

    async def fetch():
    return await execute_find(
    'pactl list cards short',
    'bluez_card.%s' % '_'.join(mac))
    if match:
    return match.group()
    elif tries > 0:
    await asyncio.sleep(LONG_SLEEP)
    print(message + retry_message % tries)
    tries -= 1
    continue

    result = await fetch()
    while tries > 0 and result is None:
    await asyncio.sleep(.5)
    tries -= 1
    print('Cannot get device id, retrying %d more times.' % (tries+1))
    result = await fetch()
    return result
    raise RetryExceededError('Retry times exceeded: %s' % message)


    async def find_sink(mac, tries=12):
    async def find_dev_id(mac, tries=12):
    return await execute_find(
    'pactl list cards short',
    'bluez_card.%s' % '_'.join(mac),
    tries=tries,
    message='Cannot list cards using `pactl`'
    )

    async def fetch():
    return await execute_find(
    'pacmd list-sinks', 'bluez_sink.%s' % '_'.join(mac))

    result = await fetch()
    while tries > 0 and result is None:
    await asyncio.sleep(.5)
    tries -= 1
    print('Cannot find any sink, retrying %d more times.' % (tries+1))
    result = await fetch()
    return result
    async def find_sink(mac, tries=12):
    return await execute_find(
    'pacmd list-sinks',
    'bluez_sink.%s' % '_'.join(mac),
    tries=tries,
    message='Cannot list sinks using `pacmd`'
    )


    async def set_profile(device_id, profile='a2dp_sink'):
    @@ -197,7 +215,7 @@ async def set_default_sink(sink):
    return await execute_command('pacmd set-default-sink %s' % sink)


    async def main(args):
    async def main(args, loop):
    mac = args.mac

    exit_future = asyncio.Future()
    @@ -209,7 +227,7 @@ async def main(args):

    if mac is None:
    print('Selecting device:')
    mac, device_name = await protocol.select_device()
    mac, _ = await protocol.select_device()

    mac = mac.split(':' if ':' in mac else '_')
    print('Device MAC: %s' % ':'.join(mac))
    @@ -236,7 +254,7 @@ async def main(args):
    print('Disconnecting the device.')
    await protocol.disconnect(mac)

    print('Connecting againt.')
    print('Connecting again.')
    await protocol.connect(mac)

    print('Setting A2DP profile')
    @@ -247,8 +265,8 @@ async def main(args):
    print('Updating default sink')
    await set_default_sink(sink)

    except SubprocessError as ex:
    print(str(ex))
    except (SubprocessError, RetryExceededError) as ex:
    print(str(ex), file=sys.stderr)
    return 1
    finally:
    print('Exiting bluetoothctl')
    @@ -258,13 +276,12 @@ async def main(args):
    # Close the stdout pipe
    transport.close()

    print('Enjoy HiFi stereo music!')

    if __name__ == '__main__':

    if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog=sys.argv[0])
    parser.add_argument('-e', '--echo', action='store_true', default=False)
    parser.add_argument('mac', nargs='?', default=None)
    args = parser.parse_args()

    loop = asyncio.get_event_loop()
    sys.exit(loop.run_until_complete(main(args)))
    main_loop = asyncio.get_event_loop()
    sys.exit(main_loop.run_until_complete(main(parser.parse_args(), main_loop)))
  29. @pylover pylover revised this gist Oct 5, 2016. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,8 @@
    #! /usr/bin/env python3.5
    """
    Fixing bluetooth stereo headphone/headset problem in ubuntu 16.04 and also debian jessie, with bluez5.
    Workaround for bug: https://bugs.launchpad.net/ubuntu/+source/indicator-sound/+bug/1577197
    Run it with python3.5 or higher after pairing/connecting the bluetooth stereo headphone.
  30. @pylover pylover revised this gist Sep 20, 2016. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions a2dp.py
    Original file line number Diff line number Diff line change
    @@ -4,6 +4,8 @@
    Workaround for bug: https://bugs.launchpad.net/ubuntu/+source/indicator-sound/+bug/1577197
    Run it with python3.5 or higher after pairing/connecting the bluetooth stereo headphone.
    Only works with bluez5.
    See `a2dp.py -h` for info.
    https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae