Merge ~sylvain-pineau/plainbox-provider-checkbox:beacon-notif-test into plainbox-provider-checkbox:master

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: cfccea5375aca495e52235832ced4e8782712b14
Merged at revision: 3671705c73281db9ef35400d27d7c9a7e167a376
Proposed branch: ~sylvain-pineau/plainbox-provider-checkbox:beacon-notif-test
Merge into: plainbox-provider-checkbox:master
Diff against target: 275 lines (+261/-0)
2 files modified
bin/gatt-notify-test.py (+241/-0)
units/bluetooth/jobs.pxu (+20/-0)
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Devices Certification Bot Needs Fixing
Maciej Kisielewski Approve
Review via email: mp+348823@code.launchpad.net

Description of the change

Generic GATT notification test to use with Bluetooth LE beacons.

Made generic by using three env settings:

- ADV_SVC_UUID: General/Configuration GATT service UUID advertised even if
  the device is not connected
- SVC_UUID: GATT service to use to enable notifications.
- MSRMT_UUID: GATT characteristic from the SVC_UUID service to trigger
  notifications.

Tested on Tillamook devices where it works when all planets are aligned.

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

Props for battling another BT story!
Code looks great, +1.
Nitpick: I found one unnecessary condition (I think, see inline), and the `pattern` name bothers me for identifying the adapter.

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

Removed the unnecessary condition.

review: Approve
Revision history for this message
Devices Certification Bot (ce-certification-qa) wrote :
Download full text (3.3 KiB)

The merge was fine but running tests failed.

[trusty] starting container
[trusty] (timing) 0.00user 0.00system 0:00.19elapsed 4%CPU (0avgtext+0avgdata 4888maxresident)k
[trusty] (timing) 0inputs+72outputs (0major+1101minor)pagefaults 0swaps
[trusty] provisioning container
[trusty] (timing) 61.78user 188.68system 8:32.38elapsed 48%CPU (0avgtext+0avgdata 68444maxresident)k
[trusty] (timing) 16inputs+1535192outputs (20major+1101722minor)pagefaults 0swaps
[trusty-testing] Starting tests...
Found a test script: ./requirements/container-tests-provider-checkbox
[trusty-testing] container-tests-provider-checkbox: FAIL
[trusty-testing] stdout: https://paste.ubuntu.com/p/xBhJRXgrdd/
[trusty-testing] stderr: https://paste.ubuntu.com/p/FFdyN3dTXj/
[trusty-testing] (timing) Command exited with non-zero status 1
[trusty-testing] (timing) 11.52user 1.44system 0:54.18elapsed 23%CPU (0avgtext+0avgdata 60816maxresident)k
[trusty-testing] (timing) 56inputs+99832outputs (0major+481305minor)pagefaults 0swaps
[trusty-testing] Fixing file permissions in source directory
[trusty-testing] Destroying container
Name: trusty-testing
State: STOPPED
[xenial] starting container
[xenial] (timing) 0.00user 0.00system 0:00.24elapsed 3%CPU (0avgtext+0avgdata 4912maxresident)k
[xenial] (timing) 0inputs+72outputs (0major+1092minor)pagefaults 0swaps
[xenial] provisioning container
[xenial] (timing) 25.49user 16.14system 4:09.40elapsed 16%CPU (0avgtext+0avgdata 82456maxresident)k
[xenial] (timing) 8inputs+1482704outputs (0major+1178032minor)pagefaults 0swaps
[xenial-testing] Starting tests...
Found a test script: ./requirements/container-tests-provider-checkbox
[xenial-testing] container-tests-provider-checkbox: FAIL
[xenial-testing] stdout: https://paste.ubuntu.com/p/44SzqKFH9h/
[xenial-testing] stderr: https://paste.ubuntu.com/p/dXqrGHVX4B/
[xenial-testing] (timing) Command exited with non-zero status 1
[xenial-testing] (timing) 12.30user 1.08system 0:29.95elapsed 44%CPU (0avgtext+0avgdata 63348maxresident)k
[xenial-testing] (timing) 0inputs+100192outputs (0major+488737minor)pagefaults 0swaps
[xenial-testing] Fixing file permissions in source directory
[xenial-testing] Destroying container
Name: xenial-testing
State: STOPPED
[bionic] starting container
[bionic] (timing) 0.00user 0.00system 0:00.24elapsed 3%CPU (0avgtext+0avgdata 4944maxresident)k
[bionic] (timing) 0inputs+72outputs (0major+1096minor)pagefaults 0swaps
[bionic] provisioning container
[bionic] (timing) 25.04user 21.74system 4:37.32elapsed 16%CPU (0avgtext+0avgdata 87984maxresident)k
[bionic] (timing) 0inputs+1490176outputs (0major+1321437minor)pagefaults 0swaps
[bionic-testing] Starting tests...
Found a test script: ./requirements/container-tests-provider-checkbox
[bionic-testing] container-tests-provider-checkbox: FAIL
[bionic-testing] stdout: https://paste.ubuntu.com/p/548wK9Pn3V/
[bionic-testing] stderr: https://paste.ubuntu.com/p/r42gzck7Gr/
[bionic-testing] (timing) Command exited with non-zero status 1
[bionic-testing] (timing) 13.19user 1.11system 0:34.14elapsed 41%CPU (0avgtext+0avgdata 55008maxresident)k
[bionic-testing] (timi...

Read more...

review: Needs Fixing
Revision history for this message
Devices Certification Bot (ce-certification-qa) wrote :

I tried to merge it but there are some problems. Typically you want to merge or rebase and try again.

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

approve once again

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/bin/gatt-notify-test.py b/bin/gatt-notify-test.py
2new file mode 100755
3index 0000000..9923a93
4--- /dev/null
5+++ b/bin/gatt-notify-test.py
6@@ -0,0 +1,241 @@
7+#!/usr/bin/env python3
8+#
9+# Copyright 2018 Canonical Ltd.
10+# Written by:
11+# Sylvain Pineau <sylvain.pineau@canonical.com>
12+#
13+# This is free software: you can redistribute it and/or modify
14+# it under the terms of the GNU General Public License version 3,
15+# as published by the Free Software Foundation.
16+#
17+# This file is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU General Public License for more details.
21+#
22+# You should have received a copy of the GNU General Public License
23+# along with this file. If not, see <http://www.gnu.org/licenses/>.
24+
25+import argparse
26+import logging
27+import os
28+import sys
29+import time
30+
31+import dbus
32+import dbus.service
33+import dbus.mainloop.glib
34+from gi.repository import GObject
35+
36+logger = logging.getLogger(__file__)
37+logger.addHandler(logging.StreamHandler(sys.stdout))
38+
39+ADAPTER_INTERFACE = 'org.bluez.Adapter1'
40+DEVICE_INTERFACE = 'org.bluez.Device1'
41+PROP_INTERFACE = 'org.freedesktop.DBus.Properties'
42+OM_INTERFACE = 'org.freedesktop.DBus.ObjectManager'
43+GATT_SERVICE_INTERFACE = 'org.bluez.GattService1'
44+GATT_CHRC_INTERFACE = 'org.bluez.GattCharacteristic1'
45+
46+dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
47+
48+
49+class BtAdapter:
50+ """Bluetooth LE Adapter class."""
51+ def __init__(self, pattern):
52+ self._pattern = os.path.basename(pattern)
53+ self._bus = dbus.SystemBus()
54+ self._manager = dbus.Interface(
55+ self._bus.get_object("org.bluez", "/"), OM_INTERFACE)
56+ self._main_loop = GObject.MainLoop()
57+ self._adapter = self._find_adapter()
58+ self._path = self._adapter.object_path
59+ self._props = dbus.Interface(self._adapter, PROP_INTERFACE)
60+ self._name = self._props.Get(ADAPTER_INTERFACE, "Name")
61+ self._addr = self._props.Get(ADAPTER_INTERFACE, "Address")
62+ self._alias = self._props.Get(ADAPTER_INTERFACE, "Alias")
63+ logger.info('Adapter found: [ {} ] {} - {}'.format(
64+ self._path, self._addr, self._alias))
65+
66+ def _get_managed_objects(self):
67+ return self._manager.GetManagedObjects()
68+
69+ def _find_adapter(self):
70+ for path, ifaces in self._get_managed_objects().items():
71+ adapter = ifaces.get(ADAPTER_INTERFACE)
72+ if adapter is None:
73+ continue
74+ if (self._pattern == adapter["Address"] or
75+ path.endswith(self._pattern)):
76+ obj = self._bus.get_object("org.bluez", path)
77+ return dbus.Interface(obj, ADAPTER_INTERFACE)
78+ raise SystemExit("Bluetooth adapter not found!")
79+
80+ def ensure_powered(self):
81+ """Turn the adapter on."""
82+ self._props.Set(ADAPTER_INTERFACE, "Powered", dbus.Boolean(1))
83+ logger.info('Adapter powered on')
84+
85+ def scan(self, timeout=10):
86+ """Scan for BT devices."""
87+ dbus.Interface(self._adapter, ADAPTER_INTERFACE).StartDiscovery()
88+ logger.info('Adapter scan on ({}s)'.format(timeout))
89+ GObject.timeout_add_seconds(timeout, self._scan_timeout)
90+ self._main_loop.run()
91+
92+ def _scan_timeout(self):
93+ dbus.Interface(self._adapter, ADAPTER_INTERFACE).StopDiscovery()
94+ logger.info('Adapter scan completed')
95+ self._main_loop.quit()
96+
97+ def find_device_with_service(self, ADV_SVC_UUID):
98+ """Find a device with a given remote service."""
99+ for path, ifaces in self._get_managed_objects().items():
100+ device = ifaces.get(DEVICE_INTERFACE)
101+ if device is None:
102+ continue
103+ logger.debug("{} {} {}".format(
104+ path, device["Address"], device["Alias"]))
105+ if ADV_SVC_UUID in device["UUIDs"] and path.startswith(self._path):
106+ obj = self._bus.get_object("org.bluez", path)
107+ logger.info('Device found: [ {} ] {} - {}'.format(
108+ path, device["Name"], device["Address"]))
109+ return dbus.Interface(obj, DEVICE_INTERFACE)
110+ raise SystemExit("Bluetooth device not found!")
111+
112+ def remove_device(self, device):
113+ """Remove the remote device object at the given path."""
114+ try:
115+ self._adapter.RemoveDevice(device)
116+ except dbus.exceptions.DBusException as msg:
117+ logging.error(msg)
118+ raise SystemExit(1)
119+ logger.info('Device properly removed')
120+
121+
122+class BtGATTRemoteService:
123+ """Bluetooth LE GATT Remote Service class."""
124+ def __init__(self, SVC_UUID, adapter, device, max_notif):
125+ self.SVC_UUID = SVC_UUID
126+ self._adapter = adapter
127+ self.device = device
128+ self._max_notif = max_notif
129+ self._notifications = 0
130+ self._bus = dbus.SystemBus()
131+ self._manager = dbus.Interface(
132+ self._bus.get_object("org.bluez", "/"), OM_INTERFACE)
133+ self._main_loop = GObject.MainLoop()
134+ self._service = self._find_service()
135+ self._path = self._service.object_path
136+
137+ def _get_managed_objects(self):
138+ return self._manager.GetManagedObjects()
139+
140+ def _find_service(self):
141+ for path, ifaces in self._get_managed_objects().items():
142+ if GATT_SERVICE_INTERFACE not in ifaces.keys():
143+ continue
144+ service = self._bus.get_object('org.bluez', path)
145+ props = dbus.Interface(service, PROP_INTERFACE)
146+ if props.Get(GATT_SERVICE_INTERFACE, "UUID") == self.SVC_UUID:
147+ logger.info('Service found: {}'.format(path))
148+ return service
149+ self._adapter.remove_device(self._device)
150+ raise SystemExit("Bluetooth Service not found!")
151+
152+ def find_chrc(self, MSRMT_UUID):
153+ for path, ifaces in self._get_managed_objects().items():
154+ if GATT_CHRC_INTERFACE not in ifaces.keys():
155+ continue
156+ chrc = self._bus.get_object('org.bluez', path)
157+ props = dbus.Interface(chrc, PROP_INTERFACE)
158+ if props.Get(GATT_CHRC_INTERFACE, "UUID") == MSRMT_UUID:
159+ logger.info('Characteristic found: {}'.format(path))
160+ return chrc
161+ self._adapter.remove_device(self._device)
162+ raise SystemExit("Bluetooth Characteristic not found!")
163+
164+ def _generic_error_cb(self, error):
165+ self._adapter.remove_device(self._device)
166+ self._main_loop.quit()
167+ raise SystemExit('D-Bus call failed: ' + str(error))
168+
169+ def _start_notify_cb(self):
170+ logger.info('Notifications enabled')
171+
172+ def _notify_timeout(self):
173+ self._adapter.remove_device(self._device)
174+ self._main_loop.quit()
175+ raise SystemExit('Notification test failed')
176+
177+ def _changed_cb(self, iface, changed_props, invalidated_props):
178+ if iface != GATT_CHRC_INTERFACE:
179+ return
180+ if not len(changed_props):
181+ return
182+ value = changed_props.get('Value', None)
183+ if not value:
184+ return
185+ logger.debug('New Notification')
186+ self._notifications += 1
187+ if self._notifications >= self._max_notif:
188+ logger.info('Notification test succeeded')
189+ self._main_loop.quit()
190+
191+ def check_notification(self, chrc, timeout=20):
192+ # Listen to PropertiesChanged signals from the BLE Measurement
193+ # Characteristic.
194+ prop_iface = dbus.Interface(chrc, PROP_INTERFACE)
195+ prop_iface.connect_to_signal("PropertiesChanged", self._changed_cb)
196+
197+ # Subscribe to BLE Measurement notifications.
198+ chrc.StartNotify(reply_handler=self._start_notify_cb,
199+ error_handler=self._generic_error_cb,
200+ dbus_interface=GATT_CHRC_INTERFACE)
201+ GObject.timeout_add_seconds(timeout, self._notify_timeout)
202+ self._main_loop.run()
203+
204+
205+def main():
206+ logger.setLevel(logging.DEBUG)
207+ parser = argparse.ArgumentParser()
208+ parser.add_argument(
209+ "id",
210+ help='Address, udev path or name (hciX) of the BT adapter')
211+ parser.add_argument(
212+ "ADV_SVC_UUID", help='Beacon Gatt configuration service UUID')
213+ parser.add_argument(
214+ "SVC_UUID", help='Beacon Gatt notification service UUID')
215+ parser.add_argument("MSRMT_UUID", help='Beacon Gatt measurement UUID')
216+ parser.add_argument(
217+ "--max-notif", "-m", type=int, default=5,
218+ help="Maximum notification threshold")
219+ args = parser.parse_args()
220+ adapter = BtAdapter(args.id)
221+ adapter.ensure_powered()
222+ adapter.scan()
223+ device = adapter.find_device_with_service(args.ADV_SVC_UUID)
224+ try:
225+ device.Connect()
226+ except dbus.exceptions.DBusException as msg:
227+ logging.error(msg)
228+ adapter.remove_device(device)
229+ raise SystemExit(1)
230+ logger.info('Device connected, waiting 10s for services to be available')
231+ time.sleep(10) # Let all the services to broadcast their UUIDs
232+ service = BtGATTRemoteService(
233+ args.SVC_UUID, adapter, device, args.max_notif)
234+ chrc = service.find_chrc(args.MSRMT_UUID)
235+ service.check_notification(chrc)
236+ try:
237+ device.Disconnect()
238+ except dbus.exceptions.DBusException as msg:
239+ logging.error(msg)
240+ adapter.remove_device(device)
241+ raise SystemExit(1)
242+ logger.info('Device properly disconnected')
243+ adapter.remove_device(device)
244+
245+
246+if __name__ == "__main__":
247+ sys.exit(main())
248diff --git a/units/bluetooth/jobs.pxu b/units/bluetooth/jobs.pxu
249index ef73a5a..1f85fa6 100644
250--- a/units/bluetooth/jobs.pxu
251+++ b/units/bluetooth/jobs.pxu
252@@ -194,3 +194,23 @@ _steps:
253 3. After it's paired and connected, enter some text with your keyboard and close the small input test tool.
254 _verification:
255 Did the Bluetooth Smart keyboard work as expected?
256+
257+unit: template
258+template-resource: device
259+template-filter: device.category == 'BLUETOOTH'
260+template-engine: jinja2
261+template-unit: job
262+id: bluetooth4/beacon_notification_{{ path }}
263+_summary: Test system can get beacon notifications on the {{ path.split('/')[-1] }} adapter
264+environ: ADV_SVC_UUID SVC_UUID MSRMT_UUID
265+command:
266+ gatt-notify-test.py {{ path.split('/')[-1] }} $ADV_SVC_UUID $SVC_UUID $MSRMT_UUID
267+plugin: shell
268+user: root
269+category_id: com.canonical.plainbox::bluetooth
270+estimated_duration: 30
271+requires:
272+ package.name == 'bluez' or snap.name == 'bluez'
273+ {%- if __on_ubuntucore__ %}
274+ connections.slot == 'bluez:service' and connections.plug == '{{ __system_env__["SNAP_NAME"] }}:bluez'
275+ {% endif -%}

Subscribers

People subscribed via source and target branches