Skip to content

Commit 2c92a18

Browse files
authored
Add files via upload
a2dp fix script
1 parent 8598d5d commit 2c92a18

File tree

3 files changed

+459
-0
lines changed

3 files changed

+459
-0
lines changed

a2dp/a2dp.py

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
#! /usr/bin/env python3.5
2+
"""
3+
4+
Fixing bluetooth stereo headphone/headset problem in ubuntu 16.04 and also debian jessie, with bluez5.
5+
6+
Workaround for bug: https://bugs.launchpad.net/ubuntu/+source/indicator-sound/+bug/1577197
7+
Run it with python3.5 or higher after pairing/connecting the bluetooth stereo headphone.
8+
9+
This will be only fixes the bluez5 problem mentioned above .
10+
11+
Licence: Freeware
12+
13+
See ``python3.5 a2dp.py -h``.
14+
15+
Shorthands:
16+
17+
$ alias speakers="a2dp.py 10:08:C1:44:AE:BC"
18+
$ alias headphones="a2dp.py 00:22:37:3D:DA:50"
19+
$ alias headset="a2dp.py 00:22:37:F8:A0:77 -p hsp"
20+
21+
$ speakers
22+
23+
24+
25+
Check here for the latest updates: https://gist.github.com/pylover/d68be364adac5f946887b85e6ed6e7ae
26+
27+
Thanks to:
28+
29+
* https://github.com/DominicWatson, for adding the ``-p/--profile`` argument.
30+
* https://github.com/IzzySoft, for mentioning wait before connecting again.
31+
* https://github.com/AmploDev, for v0.4.0
32+
* https://github.com/Mihara, for autodetect & autorun service
33+
* https://github.com/dabrovnijk, for systemd service
34+
35+
Change Log
36+
----------
37+
38+
- 0.5.2
39+
* Increasing the number of tries to 15.
40+
41+
- 0.5.2
42+
* Optimizing waits.
43+
44+
- 0.5.1
45+
* Increasing WAIT_TIME and TRIES
46+
47+
- 0.5.0
48+
* Autodetect & autorun service
49+
50+
- 0.4.1
51+
* Sorting device list
52+
53+
- 0.4.0
54+
* Adding ignore_fail argument by @AmploDev.
55+
* Sending all available streams into selected sink, after successfull connection by @AmploDev.
56+
57+
- 0.3.3
58+
* Updating default sink before turning to ``off`` profile.
59+
60+
- 0.3.2
61+
* Waiting a bit: ``-w/--wait`` before connecting again.
62+
63+
- 0.3.0
64+
* Adding -p / --profile option for using the same script to switch between headset and A2DP audio profiles
65+
66+
- 0.2.5
67+
* Mentioning [mac] argument.
68+
69+
- 0.2.4
70+
* Removing duplicated devices in select device list.
71+
72+
- 0.2.3
73+
* Matching ANSI escape characters. Tested on 16.10 & 16.04
74+
75+
- 0.2.2
76+
* Some sort of code enhancements.
77+
78+
- 0.2.0
79+
* Adding `-V/--version`, `-w/--wait` and `-t/--tries` CLI arguments.
80+
81+
- 0.1.1
82+
* Supporting the `[NEW]` prefix for devices & controllers as advised by @wdullaer
83+
* Drying the code.
84+
85+
"""
86+
87+
import sys
88+
import re
89+
import asyncio
90+
import subprocess as sb
91+
import argparse
92+
93+
94+
__version__ = '0.5.2'
95+
96+
97+
HEX_DIGIT_PATTERN = '[0-9A-F]'
98+
HEX_BYTE_PATTERN = '%s{2}' % HEX_DIGIT_PATTERN
99+
MAC_ADDRESS_PATTERN = ':'.join((HEX_BYTE_PATTERN, ) * 6)
100+
DEVICE_PATTERN = re.compile('^(?:.*\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
101+
CONTROLLER_PATTERN = re.compile('^(?:.*\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
102+
WAIT_TIME = 2.25
103+
TRIES = 15
104+
PROFILE = 'a2dp'
105+
106+
107+
_profiles = {
108+
'a2dp': 'a2dp_sink',
109+
'hsp': 'headset_head_unit',
110+
'off': 'off'
111+
}
112+
113+
# CLI Arguments
114+
parser = argparse.ArgumentParser(prog=sys.argv[0])
115+
parser.add_argument('-e', '--echo', action='store_true', default=False,
116+
help='If given, the subprocess stdout will be also printed on stdout.')
117+
parser.add_argument('-w', '--wait', default=WAIT_TIME, type=float,
118+
help='The seconds to wait for subprocess output, default is: %s' % WAIT_TIME)
119+
parser.add_argument('-t', '--tries', default=TRIES, type=int,
120+
help='The number of tries if subprocess is failed. default is: %s' % TRIES)
121+
parser.add_argument('-p', '--profile', default=PROFILE,
122+
help='The profile to switch to. available options are: hsp, a2dp. default is: %s' % PROFILE)
123+
parser.add_argument('-V', '--version', action='store_true', help='Show the version.')
124+
parser.add_argument('mac', nargs='?', default=None)
125+
126+
127+
# Exceptions
128+
class SubprocessError(Exception):
129+
pass
130+
131+
132+
class RetryExceededError(Exception):
133+
pass
134+
135+
136+
class BluetoothctlProtocol(asyncio.SubprocessProtocol):
137+
def __init__(self, exit_future, echo=True):
138+
self.exit_future = exit_future
139+
self.transport = None
140+
self.output = None
141+
self.echo = echo
142+
143+
def listen_output(self):
144+
self.output = ''
145+
146+
def not_listen_output(self):
147+
self.output = None
148+
149+
def pipe_data_received(self, fd, raw):
150+
d = raw.decode()
151+
if self.echo:
152+
print(d, end='')
153+
154+
if self.output is not None:
155+
self.output += d
156+
157+
def process_exited(self):
158+
self.exit_future.set_result(True)
159+
160+
def connection_made(self, transport):
161+
self.transport = transport
162+
print('Connection MADE')
163+
164+
async def send_command(self, c):
165+
stdin_transport = self.transport.get_pipe_transport(0)
166+
# noinspection PyProtectedMember
167+
stdin_transport._pipe.write(('%s\n' % c).encode())
168+
169+
async def search_in_output(self, expression, fail_expression=None):
170+
if self.output is None:
171+
return None
172+
173+
for l in self.output.splitlines():
174+
if fail_expression and re.search(fail_expression, l, re.IGNORECASE):
175+
raise SubprocessError('Expression "%s" failed with fail pattern: "%s"' % (l, fail_expression))
176+
177+
if re.search(expression, l, re.IGNORECASE):
178+
return True
179+
180+
async def send_and_wait(self, cmd, wait_expression, fail_expression='fail'):
181+
try:
182+
self.listen_output()
183+
await self.send_command(cmd)
184+
while not await self.search_in_output(wait_expression.lower(), fail_expression=fail_expression):
185+
await wait()
186+
finally:
187+
self.not_listen_output()
188+
189+
async def disconnect(self, mac):
190+
print('Disconnecting the device.')
191+
await self.send_and_wait('disconnect %s' % ':'.join(mac), 'Successful disconnected')
192+
193+
async def connect(self, mac):
194+
print('Connecting again.')
195+
await self.send_and_wait('connect %s' % ':'.join(mac), 'Connection successful')
196+
197+
async def trust(self, mac):
198+
await self.send_and_wait('trust %s' % ':'.join(mac), 'trust succeeded')
199+
200+
async def quit(self):
201+
await self.send_command('quit')
202+
203+
async def get_list(self, command, pattern):
204+
result = set()
205+
try:
206+
self.listen_output()
207+
await self.send_command(command)
208+
await wait()
209+
for l in self.output.splitlines():
210+
m = pattern.match(l)
211+
if m:
212+
result.add(m.groups())
213+
return sorted(list(result), key=lambda i: i[1])
214+
finally:
215+
self.not_listen_output()
216+
217+
async def list_devices(self):
218+
return await self.get_list('devices', DEVICE_PATTERN)
219+
220+
async def list_paired_devices(self):
221+
return await self.get_list('paired-devices', DEVICE_PATTERN)
222+
223+
async def list_controllers(self):
224+
return await self.get_list('list', CONTROLLER_PATTERN)
225+
226+
async def select_paired_device(self):
227+
print('Selecting device:')
228+
devices = await self.list_paired_devices()
229+
count = len(devices)
230+
231+
if count < 1:
232+
raise SubprocessError('There is no connected device.')
233+
elif count == 1:
234+
return devices[0]
235+
236+
for i, d in enumerate(devices):
237+
print('%d. %s %s' % (i+1, d[0], d[1]))
238+
print('Select device[1]:')
239+
selected = input()
240+
return devices[0 if not selected.strip() else (int(selected) - 1)]
241+
242+
243+
async def wait(delay=None):
244+
return await asyncio.sleep(WAIT_TIME or delay)
245+
246+
247+
async def execute_command(cmd, ignore_fail=False):
248+
p = await asyncio.create_subprocess_shell(cmd, stdout=sb.PIPE, stderr=sb.PIPE)
249+
stdout, stderr = await p.communicate()
250+
stdout, stderr = \
251+
stdout.decode() if stdout is not None else '', \
252+
stderr.decode() if stderr is not None else ''
253+
if p.returncode != 0 or stderr.strip() != '':
254+
message = 'Command: %s failed with status: %s\nstderr: %s' % (cmd, p.returncode, stderr)
255+
if ignore_fail:
256+
print('Ignoring: %s' % message)
257+
else:
258+
raise SubprocessError(message)
259+
return stdout
260+
261+
262+
async def execute_find(cmd, pattern, tries=0, fail_safe=False):
263+
tries = tries or TRIES
264+
265+
message = 'Cannot find `%s` using `%s`.' % (pattern, cmd)
266+
retry_message = message + ' Retrying %d more times'
267+
while True:
268+
stdout = await execute_command(cmd)
269+
match = re.search(pattern, stdout)
270+
271+
if match:
272+
return match.group()
273+
elif tries > 0:
274+
await wait()
275+
print(retry_message % tries)
276+
tries -= 1
277+
continue
278+
279+
if fail_safe:
280+
return None
281+
282+
raise RetryExceededError('Retry times exceeded: %s' % message)
283+
284+
285+
async def find_dev_id(mac, **kw):
286+
return await execute_find('pactl list cards short', 'bluez_card.%s' % '_'.join(mac), **kw)
287+
288+
289+
async def find_sink(mac, **kw):
290+
return await execute_find('pacmd list-sinks', 'bluez_sink.%s' % '_'.join(mac), **kw)
291+
292+
293+
async def set_profile(device_id, profile):
294+
print('Setting the %s profile' % profile)
295+
try:
296+
return await execute_command('pactl set-card-profile %s %s' % (device_id, _profiles[profile]))
297+
except KeyError:
298+
print('Invalid profile: %s, please select one one of a2dp or hsp.' % profile, file=sys.stderr)
299+
raise SystemExit(1)
300+
301+
302+
async def set_default_sink(sink):
303+
print('Updating default sink to %s' % sink)
304+
return await execute_command('pacmd set-default-sink %s' % sink)
305+
306+
307+
async def move_streams_to_sink(sink):
308+
streams = await execute_command('pacmd list-sink-inputs | grep "index:"', True)
309+
for i in streams.split():
310+
i = ''.join(n for n in i if n.isdigit())
311+
if i != '':
312+
print('Moving stream %s to sink' % i)
313+
await execute_command('pacmd move-sink-input %s %s' % (i, sink))
314+
return sink
315+
316+
317+
async def main(args):
318+
global WAIT_TIME, TRIES
319+
320+
if args.version:
321+
print(__version__)
322+
return 0
323+
324+
mac = args.mac
325+
326+
# Hacking, Changing the constants!
327+
WAIT_TIME = args.wait
328+
TRIES = args.tries
329+
330+
exit_future = asyncio.Future()
331+
transport, protocol = await asyncio.get_event_loop().subprocess_exec(
332+
lambda: BluetoothctlProtocol(exit_future, echo=args.echo), 'bluetoothctl'
333+
)
334+
335+
try:
336+
337+
if mac is None:
338+
mac, _ = await protocol.select_paired_device()
339+
340+
mac = mac.split(':' if ':' in mac else '_')
341+
print('Device MAC: %s' % ':'.join(mac))
342+
343+
device_id = await find_dev_id(mac, fail_safe=True)
344+
if device_id is None:
345+
print('It seems device: %s is not connected yet, trying to connect.' % ':'.join(mac))
346+
await protocol.trust(mac)
347+
await protocol.connect(mac)
348+
device_id = await find_dev_id(mac)
349+
350+
sink = await find_sink(mac, fail_safe=True)
351+
if sink is None:
352+
await set_profile(device_id, args.profile)
353+
sink = await find_sink(mac)
354+
355+
print('Device ID: %s' % device_id)
356+
print('Sink: %s' % sink)
357+
358+
await set_default_sink(sink)
359+
await wait()
360+
361+
await set_profile(device_id, 'off')
362+
363+
if args.profile is 'a2dp':
364+
await protocol.disconnect(mac)
365+
await wait()
366+
await protocol.connect(mac)
367+
368+
device_id = await find_dev_id(mac)
369+
print('Device ID: %s' % device_id)
370+
371+
await wait(2)
372+
await set_profile(device_id, args.profile)
373+
await set_default_sink(sink)
374+
await move_streams_to_sink(sink)
375+
376+
except (SubprocessError, RetryExceededError) as ex:
377+
print(str(ex), file=sys.stderr)
378+
return 1
379+
finally:
380+
print('Exiting bluetoothctl')
381+
await protocol.quit()
382+
await exit_future
383+
384+
# Close the stdout pipe
385+
transport.close()
386+
387+
if args.profile == 'a2dp':
388+
print('"Enjoy" the HiFi stereo music :)')
389+
else:
390+
print('"Enjoy" your headset audio :)')
391+
392+
393+
if __name__ == '__main__':
394+
sys.exit(asyncio.get_event_loop().run_until_complete(main(parser.parse_args())))

0 commit comments

Comments
 (0)