Merge lp:~kaxing/checkbox/bt4-bluez5 into lp:checkbox
- bt4-bluez5
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 4279 |
Merged at revision: | 4320 |
Proposed branch: | lp:~kaxing/checkbox/bt4-bluez5 |
Merge into: | lp:checkbox |
Diff against target: |
498 lines (+477/-0) 3 files modified
providers/plainbox-provider-checkbox/bin/bt_connect (+135/-0) providers/plainbox-provider-checkbox/bin/bt_helper.py (+300/-0) providers/plainbox-provider-checkbox/jobs/bluetooth.txt.in (+42/-0) |
To merge this branch: | bzr merge lp:~kaxing/checkbox/bt4-bluez5 |
Related bugs: | |
Related blueprints: |
Add test cases for Bluetooth 4.x
(Undefined)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Yung Shen (community) | Needs Resubmitting | ||
Review via email: mp+290716@code.launchpad.net |
Commit message
Description of the change
context inherited from: https:/
Add a manifest request for asking tester about BT 4.x capability.
Add a BT 4.x specific jos for HOGP devices (keyboard/mouse).
Based on bt_helper for Bluez 5.x support
known issue:
Unable to get the input() in plainbox, looking for alternative ways.
Maciej Kisielewski (kissiel) wrote : | # |
Yung Shen (kaxing) wrote : | # |
@kissiel Thanks for the review, will update base on your feedbacks. But for naming about unpairing() still needs a bit of advising. Please check.
Po-Hsu Lin (cypressyew) wrote : | # |
One small note for the break in the for loop
Yung Shen (kaxing) wrote : | # |
Update few in-line comment replies.
Maciej Kisielewski (kissiel) wrote : | # |
Following up the discussion in the inline comments
Yung Shen (kaxing) wrote : | # |
replies updating!
Yung Shen (kaxing) wrote : | # |
Updating everything based on previous in-line comments.
For print(flush=True) I've file a new bug for further discussing.(if anyone interested)
https:/
Yung Shen (kaxing) wrote : | # |
Verified on a targeting system(bt4, xenial/bluez5), everything works as expected.
Only one kind of failure the script won't able to capture:
the host system/bluez thinks it's paired but the device still blinking.
but I think this is left to tester's judgment as they are user-interact-
Sylvain Pineau (sylvain-pineau) wrote : | # |
I've proposed a workaround to the print(flush=True), see my inline suggestion.
Sylvain Pineau (sylvain-pineau) wrote : | # |
idem for the mouse job
Yung Shen (kaxing) wrote : | # |
I've removed the part that requires instant flush, so the bt_connect now is not effected to the buffered issue. those print() within flush=True is working as expected.
Po-Hsu Lin (cypressyew) wrote : | # |
Hi Yung,
from the revision history, it seems that the branch was not re-submitted successfully, the latest one is still rev 4276, Apr. 13
Yung Shen (kaxing) wrote : | # |
Hey @cypressyew, that's about right, I didn't made any changes since I've already removed the "buffered" part that mentioned in previous comments in revno 4276.
Sylvain Pineau (sylvain-pineau) wrote : | # |
I tried to run the new tests using:
plainbox run -i 2013.com.
I didn't work for two reasons:
1. Manifest entries does not support the requires statement
2. The two new jobs needs the following line to use manifest properly:
imports: from 2013.com.
Yung Shen (kaxing) wrote : | # |
Thanks for the hint, fixed manifest and add bug number in comment for buffered print().
Sylvain Pineau (sylvain-pineau) wrote : | # |
One minor fix and it should be good to go, see below
Po-Hsu Lin (cypressyew) wrote : | # |
And I think we could remove the TODO part in the comment, as this is quite mature now. (Convert print to logging might be the last thing)
Yung Shen (kaxing) wrote : | # |
So I tried logging with level=logging.info it appears there are a lot informations going on, And I think we will need to discuss this, will keep my fake logging outputs at the moment so the cli work as expected.
Yung Shen (kaxing) : | # |
Sylvain Pineau (sylvain-pineau) wrote : | # |
Those logging info could indeed be very useful. Let's merge this version and iterate.
A big +1 to all contributors. Thanks
Preview Diff
1 | === added file 'providers/plainbox-provider-checkbox/bin/bt_connect' | |||
2 | --- providers/plainbox-provider-checkbox/bin/bt_connect 1970-01-01 00:00:00 +0000 | |||
3 | +++ providers/plainbox-provider-checkbox/bin/bt_connect 2016-04-21 10:03:47 +0000 | |||
4 | @@ -0,0 +1,135 @@ | |||
5 | 1 | #!/usr/bin/env python3 | ||
6 | 2 | # | ||
7 | 3 | # This file is part of Checkbox. | ||
8 | 4 | # | ||
9 | 5 | # Copyright 2016 Canonical Ltd. | ||
10 | 6 | # | ||
11 | 7 | # Authors: | ||
12 | 8 | # Po-Hsu Lin <po-hsu.lin@canonical.com> | ||
13 | 9 | # Yung Shen <yung.shen@canonical.com> | ||
14 | 10 | # | ||
15 | 11 | # Checkbox is free software: you can redistribute it and/or modify | ||
16 | 12 | # it under the terms of the GNU General Public License version 3, | ||
17 | 13 | # as published by the Free Software Foundation. | ||
18 | 14 | # | ||
19 | 15 | # Checkbox is distributed in the hope that it will be useful, | ||
20 | 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
21 | 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
22 | 18 | # GNU General Public License for more details. | ||
23 | 19 | # | ||
24 | 20 | # You should have received a copy of the GNU General Public License | ||
25 | 21 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. | ||
26 | 22 | |||
27 | 23 | import sys | ||
28 | 24 | import time | ||
29 | 25 | |||
30 | 26 | import bt_helper | ||
31 | 27 | |||
32 | 28 | from argparse import ArgumentParser | ||
33 | 29 | |||
34 | 30 | |||
35 | 31 | def unpair_all(devices, manager): | ||
36 | 32 | """ Unpairing paired devices and scanning again for rerun jobs.""" | ||
37 | 33 | for dev in devices: | ||
38 | 34 | try: | ||
39 | 35 | print("INFO: Unpairing", dev) | ||
40 | 36 | dev.unpair() | ||
41 | 37 | except bt_helper.BtException as exc: | ||
42 | 38 | print("Warning: Unpairing failed", exc) | ||
43 | 39 | else: | ||
44 | 40 | # print(flush=True) to bypass plainbox output buffer, | ||
45 | 41 | # see LP: #1569808 for more details. | ||
46 | 42 | print("Please reset the device to pairing mode in 13 seconds", | ||
47 | 43 | flush=True) | ||
48 | 44 | time.sleep(13) | ||
49 | 45 | print("INFO: Re-scaning for devices in pairing mode", flush=True) | ||
50 | 46 | manager.scan() | ||
51 | 47 | |||
52 | 48 | |||
53 | 49 | def main(): | ||
54 | 50 | """Add argument parser here and do most of the job.""" | ||
55 | 51 | parser = ArgumentParser(description=("Bluetooth auto paring and connect. " | ||
56 | 52 | "Please select one option.")) | ||
57 | 53 | group = parser.add_mutually_exclusive_group(required=True) | ||
58 | 54 | group.add_argument("--mac", type=str, | ||
59 | 55 | help="Pair with a given MAC, not using scan result,") | ||
60 | 56 | group.add_argument("--mouse", action="store_const", | ||
61 | 57 | const="input-mouse", dest="target", | ||
62 | 58 | help="List and pair with mouse devices") | ||
63 | 59 | group.add_argument("--keyboard", action="store_const", | ||
64 | 60 | const="input-keyboard", dest="target", | ||
65 | 61 | help="List and pair with keyboard devices") | ||
66 | 62 | args = parser.parse_args() | ||
67 | 63 | |||
68 | 64 | manager = bt_helper.BtManager() | ||
69 | 65 | # Power on bluetooth adapter and scanning devices in advance. | ||
70 | 66 | manager.ensure_adapters_powered() | ||
71 | 67 | manager.scan() | ||
72 | 68 | |||
73 | 69 | if args.mac: | ||
74 | 70 | # TODO check MAC format | ||
75 | 71 | print("INFO: Trying to pair with {}".format(args.mac)) | ||
76 | 72 | device = list(manager.get_bt_devices(filters={'Address': args.mac})) | ||
77 | 73 | paired_device = list(manager.get_bt_devices( | ||
78 | 74 | filters={'Address': args.mac, 'Paired': True})) | ||
79 | 75 | if not device: | ||
80 | 76 | print("ERROR: No pairable device found, terminating") | ||
81 | 77 | return 1 | ||
82 | 78 | |||
83 | 79 | unpair_all(paired_device, manager) | ||
84 | 80 | |||
85 | 81 | for dev in device: | ||
86 | 82 | try: | ||
87 | 83 | dev.pair() | ||
88 | 84 | except bt_helper.BtException as exc: | ||
89 | 85 | print("ERROR: Unable to pair: ", exc) | ||
90 | 86 | return 1 | ||
91 | 87 | else: | ||
92 | 88 | print("INFO: Device paired") | ||
93 | 89 | return 0 | ||
94 | 90 | else: | ||
95 | 91 | print("INFO: Listing targeting devices") | ||
96 | 92 | # Listing device based on RSSI | ||
97 | 93 | paired_targets = list(manager.get_bt_devices(category=bt_helper.BT_ANY, | ||
98 | 94 | filters={'Paired': True, 'Icon': args.target})) | ||
99 | 95 | if not paired_targets: | ||
100 | 96 | print("INFO: No paired targeting devices found") | ||
101 | 97 | manager.scan() | ||
102 | 98 | else: | ||
103 | 99 | unpair_all(paired_targets, manager) | ||
104 | 100 | |||
105 | 101 | target_devices = sorted(manager.get_bt_devices( | ||
106 | 102 | category=bt_helper.BT_ANY, filters={ | ||
107 | 103 | 'Paired': False, 'Icon': args.target}), | ||
108 | 104 | key=lambda x: int(x.rssi or -255), reverse=True) | ||
109 | 105 | if not target_devices: | ||
110 | 106 | print("ERROR: No target devices found, terminating") | ||
111 | 107 | return 1 | ||
112 | 108 | print("INFO: Detected devices (sorted by RSSI; highest first).") | ||
113 | 109 | # let's assing numbers to devices | ||
114 | 110 | devices = dict(enumerate(target_devices, 1)) | ||
115 | 111 | for num, dev in devices.items(): | ||
116 | 112 | print("{}. {} (RSSI: {})".format(num, dev, dev.rssi)) | ||
117 | 113 | chosen = False | ||
118 | 114 | while not chosen: | ||
119 | 115 | print("Which one would you like to connect to? (0 to exit)") | ||
120 | 116 | num = input() | ||
121 | 117 | # TODO: enter as default to 1st device | ||
122 | 118 | if num == '0': | ||
123 | 119 | return 1 | ||
124 | 120 | chosen = num.isnumeric() and int(num) in devices.keys() | ||
125 | 121 | print("INFO: {} chosen.".format(devices[int(num)])) | ||
126 | 122 | print("INFO: Pairing selected device..") | ||
127 | 123 | try: | ||
128 | 124 | devices[int(num)].pair() | ||
129 | 125 | except bt_helper.BtException as exc: | ||
130 | 126 | print("ERROR: something wrong: ", exc) | ||
131 | 127 | return 1 | ||
132 | 128 | else: | ||
133 | 129 | print("Paired successfully.") | ||
134 | 130 | return 0 | ||
135 | 131 | # capture all other silence failures | ||
136 | 132 | return 1 | ||
137 | 133 | |||
138 | 134 | if __name__ == "__main__": | ||
139 | 135 | sys.exit(main()) | ||
140 | 0 | 136 | ||
141 | === added file 'providers/plainbox-provider-checkbox/bin/bt_helper.py' | |||
142 | --- providers/plainbox-provider-checkbox/bin/bt_helper.py 1970-01-01 00:00:00 +0000 | |||
143 | +++ providers/plainbox-provider-checkbox/bin/bt_helper.py 2016-04-21 10:03:47 +0000 | |||
144 | @@ -0,0 +1,300 @@ | |||
145 | 1 | # Copyright 2016 Canonical Ltd. | ||
146 | 2 | # Written by: | ||
147 | 3 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> | ||
148 | 4 | # | ||
149 | 5 | # This is free software: you can redistribute it and/or modify | ||
150 | 6 | # it under the terms of the GNU General Public License version 3, | ||
151 | 7 | # as published by the Free Software Foundation. | ||
152 | 8 | # | ||
153 | 9 | # This file is distributed in the hope that it will be useful, | ||
154 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
155 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
156 | 12 | # GNU General Public License for more details. | ||
157 | 13 | # | ||
158 | 14 | # You should have received a copy of the GNU General Public License | ||
159 | 15 | # along with this file. If not, see <http://www.gnu.org/licenses/>. | ||
160 | 16 | """ | ||
161 | 17 | This module provides a set of abstractions to ease the process of automating | ||
162 | 18 | typical Bluetooth task like scanning for devices and pairing with them. | ||
163 | 19 | |||
164 | 20 | It talks with BlueZ stack using dbus. | ||
165 | 21 | """ | ||
166 | 22 | import logging | ||
167 | 23 | |||
168 | 24 | import dbus | ||
169 | 25 | import dbus.service | ||
170 | 26 | import dbus.mainloop.glib | ||
171 | 27 | from gi.repository import GObject | ||
172 | 28 | |||
173 | 29 | logger = logging.getLogger(__file__) | ||
174 | 30 | logger.addHandler(logging.StreamHandler()) | ||
175 | 31 | |||
176 | 32 | IFACE = 'org.bluez.Adapter1' | ||
177 | 33 | ADAPTER_IFACE = 'org.bluez.Adapter1' | ||
178 | 34 | DEVICE_IFACE = 'org.bluez.Device1' | ||
179 | 35 | AGENT_IFACE = 'org.bluez.Agent1' | ||
180 | 36 | |||
181 | 37 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) | ||
182 | 38 | |||
183 | 39 | # To get additional Bluetoot CoDs, check | ||
184 | 40 | # https://www.bluetooth.com/specifications/assigned-numbers/baseband | ||
185 | 41 | BT_ANY = 0 | ||
186 | 42 | BT_KEYBOARD = int('0x2540', 16) | ||
187 | 43 | |||
188 | 44 | |||
189 | 45 | class BtException(Exception): | ||
190 | 46 | pass | ||
191 | 47 | |||
192 | 48 | |||
193 | 49 | class BtManager: | ||
194 | 50 | """ Main point of contact with dbus factoring bt objects. """ | ||
195 | 51 | def __init__(self, verbose=False): | ||
196 | 52 | if verbose: | ||
197 | 53 | logger.setLevel(logging.DEBUG) | ||
198 | 54 | self._bus = dbus.SystemBus() | ||
199 | 55 | self._bt_root = self._bus.get_object('org.bluez', '/') | ||
200 | 56 | self._manager = dbus.Interface( | ||
201 | 57 | self._bt_root, 'org.freedesktop.DBus.ObjectManager') | ||
202 | 58 | self._main_loop = GObject.MainLoop() | ||
203 | 59 | self._register_agent() | ||
204 | 60 | |||
205 | 61 | def _register_agent(self): | ||
206 | 62 | path = "/bt_helper/agent" | ||
207 | 63 | BtAgent(self._bus, path) | ||
208 | 64 | obj = self._bus.get_object('org.bluez', "/org/bluez") | ||
209 | 65 | agent_manager = dbus.Interface(obj, "org.bluez.AgentManager1") | ||
210 | 66 | agent_manager.RegisterAgent(path, 'NoInputNoOutput') | ||
211 | 67 | logger.info("Agent registered") | ||
212 | 68 | |||
213 | 69 | def _get_objects_by_iface(self, iface_name): | ||
214 | 70 | for path, ifaces in self._manager.GetManagedObjects().items(): | ||
215 | 71 | if ifaces.get(iface_name): | ||
216 | 72 | yield self._bus.get_object('org.bluez', path) | ||
217 | 73 | |||
218 | 74 | def get_bt_adapters(self): | ||
219 | 75 | """Yield BtAdapter objects for each BT adapter found.""" | ||
220 | 76 | for adapter in self._get_objects_by_iface(ADAPTER_IFACE): | ||
221 | 77 | yield BtAdapter(dbus.Interface(adapter, ADAPTER_IFACE), self) | ||
222 | 78 | |||
223 | 79 | def get_bt_devices(self, category=BT_ANY, filters={}): | ||
224 | 80 | """Yields BtDevice objects currently known to the system. | ||
225 | 81 | |||
226 | 82 | filters - specifies the characteristics of that a BT device must have | ||
227 | 83 | to be yielded. The keys of filters dictionary represent names of | ||
228 | 84 | parameters (as specified by the bluetooth DBus Api and represented by | ||
229 | 85 | DBus proxy object), and its values must match proxy values. | ||
230 | 86 | I.e. {'Paired': False}. For a full list of Parameters see: | ||
231 | 87 | http://git.kernel.org/cgit/bluetooth/bluez.git/tree/doc/device-api.txt | ||
232 | 88 | |||
233 | 89 | Note that this function returns objects corresponding to BT devices | ||
234 | 90 | that were seen last time scanning was done.""" | ||
235 | 91 | for device in self._get_objects_by_iface(DEVICE_IFACE): | ||
236 | 92 | obj = self.get_object_by_path(device.object_path)[DEVICE_IFACE] | ||
237 | 93 | try: | ||
238 | 94 | if category != BT_ANY: | ||
239 | 95 | if obj['Class'] != category: | ||
240 | 96 | continue | ||
241 | 97 | rejected = False | ||
242 | 98 | for filter in filters: | ||
243 | 99 | if obj[filter] != filters[filter]: | ||
244 | 100 | rejected = True | ||
245 | 101 | break | ||
246 | 102 | if rejected: | ||
247 | 103 | continue | ||
248 | 104 | yield BtDevice(dbus.Interface(device, DEVICE_IFACE), self) | ||
249 | 105 | except KeyError as exc: | ||
250 | 106 | logger.info('Property %s not found on device %s', | ||
251 | 107 | exc, device.object_path) | ||
252 | 108 | continue | ||
253 | 109 | |||
254 | 110 | def get_prop_iface(self, obj): | ||
255 | 111 | return dbus.Interface(self._bus.get_object( | ||
256 | 112 | 'org.bluez', obj.object_path), 'org.freedesktop.DBus.Properties') | ||
257 | 113 | |||
258 | 114 | def get_object_by_path(self, path): | ||
259 | 115 | return self._manager.GetManagedObjects()[path] | ||
260 | 116 | |||
261 | 117 | def get_proxy_by_path(self, path): | ||
262 | 118 | return self._bus.get_object('org.bluez', path) | ||
263 | 119 | |||
264 | 120 | def wait(self): | ||
265 | 121 | self._main_loop.run() | ||
266 | 122 | |||
267 | 123 | def quit_loop(self): | ||
268 | 124 | self._main_loop.quit() | ||
269 | 125 | |||
270 | 126 | def ensure_adapters_powered(self): | ||
271 | 127 | for adapter in self.get_bt_adapters(): | ||
272 | 128 | adapter.ensure_powered() | ||
273 | 129 | |||
274 | 130 | def scan(self, timeout=10): | ||
275 | 131 | """Scan for BT devices visible to all adapters.'""" | ||
276 | 132 | self._bus.add_signal_receiver( | ||
277 | 133 | interfaces_added, | ||
278 | 134 | dbus_interface="org.freedesktop.DBus.ObjectManager", | ||
279 | 135 | signal_name="InterfacesAdded") | ||
280 | 136 | self._bus.add_signal_receiver( | ||
281 | 137 | properties_changed, | ||
282 | 138 | dbus_interface="org.freedesktop.DBus.Properties", | ||
283 | 139 | signal_name="PropertiesChanged", | ||
284 | 140 | arg0="org.bluez.Device1", | ||
285 | 141 | path_keyword="path") | ||
286 | 142 | for adapter in self._get_objects_by_iface(ADAPTER_IFACE): | ||
287 | 143 | try: | ||
288 | 144 | dbus.Interface(adapter, ADAPTER_IFACE).StopDiscovery() | ||
289 | 145 | except dbus.exceptions.DBusException: | ||
290 | 146 | pass | ||
291 | 147 | dbus.Interface(adapter, ADAPTER_IFACE).StartDiscovery() | ||
292 | 148 | GObject.timeout_add_seconds(timeout, self._scan_timeout) | ||
293 | 149 | self._main_loop.run() | ||
294 | 150 | |||
295 | 151 | def get_devices(self, timeout=10, rescan=True): | ||
296 | 152 | """Scan for and list all devices visible to all adapters.""" | ||
297 | 153 | if rescan: | ||
298 | 154 | self.scan(timeout) | ||
299 | 155 | return list(self.get_bt_devices()) | ||
300 | 156 | |||
301 | 157 | def _scan_timeout(self): | ||
302 | 158 | for adapter in self._get_objects_by_iface(ADAPTER_IFACE): | ||
303 | 159 | dbus.Interface(adapter, ADAPTER_IFACE).StopDiscovery() | ||
304 | 160 | self._main_loop.quit() | ||
305 | 161 | |||
306 | 162 | |||
307 | 163 | class BtAdapter: | ||
308 | 164 | def __init__(self, dbus_iface, bt_mgr): | ||
309 | 165 | self._if = dbus_iface | ||
310 | 166 | self._bt_mgr = bt_mgr | ||
311 | 167 | self._prop_if = bt_mgr.get_prop_iface(dbus_iface) | ||
312 | 168 | |||
313 | 169 | def set_bool_prop(self, prop_name, value): | ||
314 | 170 | self._prop_if.Set(IFACE, prop_name, dbus.Boolean(value)) | ||
315 | 171 | |||
316 | 172 | def ensure_powered(self): | ||
317 | 173 | """Turn the adapter on, and do nothing if already on.""" | ||
318 | 174 | powered = self._prop_if.Get(IFACE, 'Powered') | ||
319 | 175 | logger.info('Powering on {}'.format( | ||
320 | 176 | self._if.object_path.split('/')[-1])) | ||
321 | 177 | if powered: | ||
322 | 178 | logger.info('Device already powered') | ||
323 | 179 | return | ||
324 | 180 | try: | ||
325 | 181 | self.set_bool_prop('Powered', True) | ||
326 | 182 | logger.info('Powered on') | ||
327 | 183 | except Exception as exc: | ||
328 | 184 | logging.error('Failed to power on - {}'.format( | ||
329 | 185 | exc.get_dbus_message())) | ||
330 | 186 | |||
331 | 187 | |||
332 | 188 | class BtDevice: | ||
333 | 189 | def __init__(self, dbus_iface, bt_mgr): | ||
334 | 190 | self._if = dbus_iface | ||
335 | 191 | self._obj = bt_mgr.get_object_by_path( | ||
336 | 192 | self._if.object_path)[DEVICE_IFACE] | ||
337 | 193 | self._bt_mgr = bt_mgr | ||
338 | 194 | self._prop_if = bt_mgr.get_prop_iface(dbus_iface) | ||
339 | 195 | self._pair_outcome = None | ||
340 | 196 | |||
341 | 197 | def __str__(self): | ||
342 | 198 | return "{} ({})".format(self.name, self.address) | ||
343 | 199 | |||
344 | 200 | def __repr__(self): | ||
345 | 201 | return "<BtDevice name:{}, address:{}>".format(self.name, self.address) | ||
346 | 202 | |||
347 | 203 | def pair(self): | ||
348 | 204 | """Pair the device. | ||
349 | 205 | |||
350 | 206 | This function will try pairing with the device and block until device | ||
351 | 207 | is paired, error occured or default timeout elapsed (whichever comes | ||
352 | 208 | first). | ||
353 | 209 | """ | ||
354 | 210 | self._prop_if.Set(DEVICE_IFACE, 'Trusted', True) | ||
355 | 211 | self._if.Pair( | ||
356 | 212 | reply_handler=self._pair_ok, error_handler=self._pair_error) | ||
357 | 213 | self._bt_mgr.wait() | ||
358 | 214 | if self._pair_outcome: | ||
359 | 215 | raise BtException(self._pair_outcome) | ||
360 | 216 | try: | ||
361 | 217 | self._if.Connect() | ||
362 | 218 | except dbus.exceptions.DBusException as exc: | ||
363 | 219 | logging.error('Failed to connect - {}'.format( | ||
364 | 220 | exc.get_dbus_message())) | ||
365 | 221 | |||
366 | 222 | def unpair(self): | ||
367 | 223 | self._if.Disconnect() | ||
368 | 224 | adapter = self._bt_mgr.get_proxy_by_path(self._obj['Adapter']) | ||
369 | 225 | dbus.Interface(adapter, ADAPTER_IFACE).RemoveDevice(self._if) | ||
370 | 226 | |||
371 | 227 | @property | ||
372 | 228 | def name(self): | ||
373 | 229 | return self._obj.get('Name', '<Unnamed>') | ||
374 | 230 | |||
375 | 231 | @property | ||
376 | 232 | def address(self): | ||
377 | 233 | return self._obj['Address'] | ||
378 | 234 | |||
379 | 235 | @property | ||
380 | 236 | def rssi(self): | ||
381 | 237 | return self._obj.get('RSSI', None) | ||
382 | 238 | |||
383 | 239 | def _pair_ok(self): | ||
384 | 240 | logger.info('%s successfully paired', self.name) | ||
385 | 241 | self._pair_outcome = None | ||
386 | 242 | self._bt_mgr.quit_loop() | ||
387 | 243 | |||
388 | 244 | def _pair_error(self, error): | ||
389 | 245 | logger.warning('Pairing of %s device failed. %s', self.name, error) | ||
390 | 246 | self._pair_outcome = error | ||
391 | 247 | self._bt_mgr.quit_loop() | ||
392 | 248 | |||
393 | 249 | |||
394 | 250 | class Rejected(dbus.DBusException): | ||
395 | 251 | _dbus_error_name = "org.bluez.Error.Rejected" | ||
396 | 252 | |||
397 | 253 | |||
398 | 254 | class BtAgent(dbus.service.Object): | ||
399 | 255 | """Agent authenticating everything that is possible.""" | ||
400 | 256 | @dbus.service.method(AGENT_IFACE, in_signature="os", out_signature="") | ||
401 | 257 | def AuthorizeService(self, device, uuid): | ||
402 | 258 | logger.info("AuthorizeService (%s, %s)", device, uuid) | ||
403 | 259 | |||
404 | 260 | @dbus.service.method(AGENT_IFACE, in_signature="o", out_signature="u") | ||
405 | 261 | def RequestPasskey(self, device): | ||
406 | 262 | logger.info("RequestPasskey (%s)", device) | ||
407 | 263 | passkey = input("Enter passkey: ") | ||
408 | 264 | return dbus.UInt32(passkey) | ||
409 | 265 | |||
410 | 266 | @dbus.service.method(AGENT_IFACE, in_signature="o", out_signature="s") | ||
411 | 267 | def RequestPinCode(self, device): | ||
412 | 268 | logger.info("RequestPinCode (%s)", device) | ||
413 | 269 | return input("Enter PIN Code: ") | ||
414 | 270 | |||
415 | 271 | @dbus.service.method(AGENT_IFACE, in_signature="ouq", out_signature="") | ||
416 | 272 | def DisplayPasskey(self, device, passkey, entered): | ||
417 | 273 | print("DisplayPasskey (%s, %06u entered %u)" % | ||
418 | 274 | (device, passkey, entered), flush=True) | ||
419 | 275 | |||
420 | 276 | @dbus.service.method(AGENT_IFACE, in_signature="os", out_signature="") | ||
421 | 277 | def DisplayPinCode(self, device, pincode): | ||
422 | 278 | logger.info("DisplayPinCode (%s, %s)", device, pincode) | ||
423 | 279 | print('Type following pin on your device: {}'.format(pincode), | ||
424 | 280 | flush=True) | ||
425 | 281 | |||
426 | 282 | @dbus.service.method(AGENT_IFACE, in_signature="ou", out_signature="") | ||
427 | 283 | def RequestConfirmation(self, device, passkey): | ||
428 | 284 | logger.info("RequestConfirmation (%s, %06d)", device, passkey) | ||
429 | 285 | |||
430 | 286 | @dbus.service.method(AGENT_IFACE, in_signature="o", out_signature="") | ||
431 | 287 | def RequestAuthorization(self, device): | ||
432 | 288 | logger.info("RequestAuthorization (%s)", device) | ||
433 | 289 | |||
434 | 290 | @dbus.service.method(AGENT_IFACE, in_signature="", out_signature="") | ||
435 | 291 | def Cancel(self): | ||
436 | 292 | logger.info("Cancelled") | ||
437 | 293 | |||
438 | 294 | |||
439 | 295 | def properties_changed(interface, changed, invalidated, path): | ||
440 | 296 | logger.info('Property changed for device @ %s. Change: %s', path, changed) | ||
441 | 297 | |||
442 | 298 | |||
443 | 299 | def interfaces_added(path, interfaces): | ||
444 | 300 | logger.info('Added new bt interfaces: %s @ %s', interfaces, path) | ||
445 | 0 | 301 | ||
446 | === modified file 'providers/plainbox-provider-checkbox/jobs/bluetooth.txt.in' | |||
447 | --- providers/plainbox-provider-checkbox/jobs/bluetooth.txt.in 2016-01-12 10:28:35 +0000 | |||
448 | +++ providers/plainbox-provider-checkbox/jobs/bluetooth.txt.in 2016-04-21 10:03:47 +0000 | |||
449 | @@ -1,3 +1,8 @@ | |||
450 | 1 | unit: manifest entry | ||
451 | 2 | id: has_bt_smart | ||
452 | 3 | _name: Bluetooth Smart (4.0 or later) Support | ||
453 | 4 | value-type: bool | ||
454 | 5 | |||
455 | 1 | plugin: shell | 6 | plugin: shell |
456 | 2 | category_id: 2013.com.canonical.plainbox::bluetooth | 7 | category_id: 2013.com.canonical.plainbox::bluetooth |
457 | 3 | id: bluetooth/detect-output | 8 | id: bluetooth/detect-output |
458 | @@ -157,3 +162,40 @@ | |||
459 | 157 | retrieves it again using Bluetooth and verifies the checksum to ensure the | 162 | retrieves it again using Bluetooth and verifies the checksum to ensure the |
460 | 158 | transfer didn't corrupt the file. | 163 | transfer didn't corrupt the file. |
461 | 159 | 164 | ||
462 | 165 | plugin: user-interact-verify | ||
463 | 166 | category_id: 2013.com.canonical.plainbox::bluetooth | ||
464 | 167 | id: bluetooth4/HOGP-mouse | ||
465 | 168 | depends: bluetooth/detect-output | ||
466 | 169 | imports: from 2013.com.canonical.plainbox import manifest | ||
467 | 170 | requires: | ||
468 | 171 | manifest.has_bt_smart == 'True' | ||
469 | 172 | package.name == 'bluez' and package.version >= '5.37' | ||
470 | 173 | estimated_duration: 30.0 | ||
471 | 174 | command: bt_connect-weak-logging --mouse | ||
472 | 175 | _purpose: | ||
473 | 176 | This test will check that you can use a HID Over GATT Profile (HOGP) with your Bluetooth Smart mouse. | ||
474 | 177 | _steps: | ||
475 | 178 | 1. Enable a Bluetooth smart mouse, and put it into paring mode. | ||
476 | 179 | 2. Commence the test to do the auto-pairing, you will be asked to select targeting mouse from the list. | ||
477 | 180 | 3. After it's paired and connected, perform actions such as moving the pointer, right and left button clicks and double clicks. | ||
478 | 181 | _verification: | ||
479 | 182 | Did the Bluetooth Smart mouse work as expected? | ||
480 | 183 | |||
481 | 184 | plugin: user-interact-verify | ||
482 | 185 | category_id: 2013.com.canonical.plainbox::bluetooth | ||
483 | 186 | id: bluetooth4/HOGP-keyboard | ||
484 | 187 | depends: bluetooth/detect-output | ||
485 | 188 | imports: from 2013.com.canonical.plainbox import manifest | ||
486 | 189 | requires: | ||
487 | 190 | manifest.has_bt_smart == 'True' | ||
488 | 191 | package.name == 'bluez' and package.version >= '5.37' | ||
489 | 192 | estimated_duration: 30.0 | ||
490 | 193 | command: bt_connect --keyboard | ||
491 | 194 | _purpose: | ||
492 | 195 | This test will check that you can use a HID Over GATT Profile (HOGP) with your Bluetooth Smart keyboard. | ||
493 | 196 | _steps: | ||
494 | 197 | 1. Enable a Bluetooth Smart keyboard, and put it into paring mode. | ||
495 | 198 | 2. Commence the test to do the auto-pairing, you will be asked to select targeting keyboard from the list. | ||
496 | 199 | 3. After it's paired and connected, enter some text with your keyboard and close the small input test tool. | ||
497 | 200 | _verification: | ||
498 | 201 | Did the Bluetooth Smart keyboard work as expected? |
I proposed a few improvements over tiny details inline.