Merge lp:~kaxing/checkbox/bt4-bluez5 into lp:checkbox

Proposed by Yung Shen
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
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Yung Shen (community) Needs Resubmitting
Review via email: mp+290716@code.launchpad.net

Description of the change

context inherited from: https://code.launchpad.net/~cypressyew/checkbox/bt4-HOGP/+merge/288612

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.

To post a comment you must log in.
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

I proposed a few improvements over tiny details inline.

Revision history for this message
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.

Revision history for this message
Po-Hsu Lin (cypressyew) wrote :

One small note for the break in the for loop

Revision history for this message
Yung Shen (kaxing) wrote :

Update few in-line comment replies.

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Following up the discussion in the inline comments

Revision history for this message
Yung Shen (kaxing) wrote :

replies updating!

lp:~kaxing/checkbox/bt4-bluez5 updated
4276. By Yung Shen

Update bt_connect: with new unpair_all and new-way of description for HOGP jobs

Revision history for this message
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://bugs.launchpad.net/plainbox/+bug/1569808

review: Needs Resubmitting
Revision history for this message
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-verify jobs.

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

I've proposed a workaround to the print(flush=True), see my inline suggestion.

review: Needs Fixing
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

idem for the mouse job

Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
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

Revision history for this message
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.

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

I tried to run the new tests using:

plainbox run -i 2013.com.canonical.plainbox::collect-manifest -i .*bluetooth4.*

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.canonical.plainbox import manifest

review: Needs Fixing
Revision history for this message
Yung Shen (kaxing) wrote :

Thanks for the hint, fixed manifest and add bug number in comment for buffered print().

review: Needs Resubmitting
lp:~kaxing/checkbox/bt4-bluez5 updated
4277. By Yung Shen

Fix bluetooth.txt.in: add missing 'imports' for manifest requirements

4278. By Yung Shen

Add bug number to bt_connect about print(flush=True) buffering issue

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

One minor fix and it should be good to go, see below

review: Needs Fixing
Revision history for this message
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)

Revision history for this message
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.

Revision history for this message
Yung Shen (kaxing) :
review: Needs Resubmitting
lp:~kaxing/checkbox/bt4-bluez5 updated
4279. By Yung Shen

Update bluetooth.txt.in remove unecessary bits under manifest

Revision history for this message
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

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches