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

Subscribers

People subscribed via source and target branches