autotest: Adds GATT Read/write support.
BUG=b:29448871
TEST=Sample jetstream test which reads/writes using
GATT, using samus as the peripheral.
Change-Id: I200fa125cd367f70c8de76f96f2b7b7f752e6041
Reviewed-on: https://chromium-review.googlesource.com/550208
Commit-Ready: Dane Pollock <danepollock@google.com>
Tested-by: Dane Pollock <danepollock@google.com>
Reviewed-by: Shyh-In Hwang <josephsih@chromium.org>
diff --git a/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py b/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
index 9a562ee..d2918e7 100755
--- a/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
+++ b/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
@@ -4,6 +4,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import base64
import dbus
import dbus.mainloop.glib
import dbus.service
@@ -13,7 +14,6 @@
import logging.handlers
import os
import shutil
-import time
import common
from autotest_lib.client.bin import utils
@@ -24,6 +24,20 @@
from autotest_lib.client.cros.bluetooth import output_recorder
+def _dbus_byte_array_to_b64_string(dbus_byte_array):
+ """Base64 encodes a dbus byte array for use with the xml rpc proxy."""
+ return base64.standard_b64encode(bytearray(dbus_byte_array))
+
+
+def _b64_string_to_dbus_byte_array(b64_string):
+ """Base64 decodes a dbus byte array for use with the xml rpc proxy."""
+ dbus_array = dbus.Array([], signature=dbus.Signature('y'))
+ bytes = bytearray(base64.standard_b64decode(b64_string))
+ for byte in bytes:
+ dbus_array.append(dbus.Byte(byte))
+ return dbus_array
+
+
class PairingAgent(dbus.service.Object):
"""The agent handling the authentication process of bluetooth pairing.
@@ -85,6 +99,7 @@
BLUEZ_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager'
BLUEZ_ADAPTER_IFACE = 'org.bluez.Adapter1'
BLUEZ_DEVICE_IFACE = 'org.bluez.Device1'
+ BLUEZ_GATT_IFACE = 'org.bluez.GattCharacteristic1'
BLUEZ_LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
BLUEZ_AGENT_MANAGER_PATH = '/org/bluez'
BLUEZ_AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1'
@@ -753,6 +768,27 @@
@returns: An 'org.bluez.Device1' interface to the device.
None if device can not be found.
+ """
+ path = self._get_device_path(address)
+ if path:
+ obj = self._system_bus.get_object(
+ self.BLUEZ_SERVICE_NAME, path)
+ return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE)
+ logging.info('Device not found')
+ return None
+
+
+ @xmlrpc_server.dbus_safe(False)
+ def _get_device_path(self, address):
+ """Gets the path for a device with a given address.
+
+ Find the device with a given address and returns the
+ the path for the device.
+
+ @param address: Address of the device.
+
+ @returns: The path to the address of the device, or None if device is
+ not found in the object tree.
"""
objects = self._bluez.GetManagedObjects(
@@ -763,16 +799,13 @@
continue
if (device['Address'] == address and
path.startswith(self._adapter.object_path)):
- obj = self._system_bus.get_object(
- self.BLUEZ_SERVICE_NAME, path)
- return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE)
- logging.info('Device not found')
- return None
+ return path
+ logging.info('Device path not found')
@xmlrpc_server.dbus_safe(False)
def _setup_pairing_agent(self, pin):
- """Initializes and resiters a PairingAgent to handle authenticaiton.
+ """Initializes and resiters a PairingAgent to handle authentication.
@param pin: The pin code this agent will answer.
@@ -831,7 +864,7 @@
@xmlrpc_server.dbus_safe(False)
- def _is_connected(self, device):
+ def _is_connected(self, device):
"""Checks if a device is connected.
@param device: An 'org.bluez.Device1' interface to the device.
@@ -845,6 +878,7 @@
return bool(connected)
+
@xmlrpc_server.dbus_safe(False)
def _set_trusted_by_device(self, device, trusted=True):
"""Set the device trusted by device object.
@@ -936,7 +970,7 @@
return True
device_path = device.object_path
- logging.info('Device %s is found.' % device.object_path)
+ logging.info('Device %s is found.', device.object_path)
self._setup_pairing_agent(pin)
mainloop = gobject.MainLoop()
@@ -1057,6 +1091,45 @@
return not self._is_connected(device)
+ @xmlrpc_server.dbus_safe(False)
+ def _device_services_resolved(self, device):
+ """Checks if services are resolved.
+
+ @param device: An 'org.bluez.Device1' interface to the device.
+
+ @returns: True if device is connected. False otherwise.
+
+ """
+ logging.info('device for services resolved: %s', device)
+ props = dbus.Interface(device, dbus.PROPERTIES_IFACE)
+ resolved = props.Get(self.BLUEZ_DEVICE_IFACE, 'ServicesResolved')
+ logging.info('Services resolved = %r', resolved)
+ return bool(resolved)
+
+
+ @xmlrpc_server.dbus_safe(False)
+ def device_services_resolved(self, address):
+ """Checks if service discovery is complete on a device.
+
+ Checks whether service discovery has been completed..
+
+ @param address: Address of the remote device.
+
+ @returns: True on success. False otherwise.
+
+ """
+ device = self._find_device(address)
+ if not device:
+ logging.error('Device not found')
+ return False
+
+ if not self._is_connected(device):
+ logging.info('Device is not connected')
+ return False
+
+ return self._device_services_resolved(device)
+
+
def btmon_start(self):
"""Start btmon monitoring."""
self.btmon.start()
@@ -1245,6 +1318,123 @@
'reset_advertising: failed: %s', str(error)))
+ @xmlrpc_server.dbus_safe(False)
+ def get_characteristic_map(self, address):
+ """Gets a map of characteristic paths for a device.
+
+ Walks the object tree, and returns a map of uuids to object paths for
+ all resolved gatt characteristics.
+
+ @param address: The MAC address of the device to retrieve
+ gatt characteristic uuids and paths from.
+
+ @returns: A dictionary of characteristic paths, keyed by uuid.
+
+ """
+ device_path = self._get_device_path(address)
+ char_map = {}
+
+ if device_path:
+ objects = self._bluez.GetManagedObjects(
+ dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=False)
+
+ for path, ifaces in objects.iteritems():
+ if (self.BLUEZ_GATT_IFACE in ifaces and
+ path.startswith(device_path)):
+ uuid = ifaces[self.BLUEZ_GATT_IFACE]['UUID'].lower()
+ char_map[uuid] = path
+ else:
+ logging.warning('Device %s not in object tree.', address)
+
+ return char_map
+
+
+ @xmlrpc_server.dbus_safe(False)
+ def _get_char_object(self, uuid, address):
+ """Gets a characteristic object.
+
+ Gets a characteristic object for a given uuid and address.
+
+ @param uuid: The uuid of the characteristic, as a string.
+ @param address: The MAC address of the remote device.
+
+ @returns: A dbus interface for the characteristic if the uuid/address
+ is in the object tree.
+ None if the address/uuid is not found in the object tree.
+
+ """
+ path = self.get_characteristic_map(address).get(uuid)
+ if not path:
+ return None
+ return dbus.Interface(
+ self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, path),
+ self.BLUEZ_GATT_IFACE)
+
+
+ @xmlrpc_server.dbus_safe(None)
+ def read_characteristic(self, uuid, address):
+ """Reads the value of a gatt characteristic.
+
+ Reads the current value of a gatt characteristic. Base64 endcoding is
+ used for compatibility with the XML RPC interface.
+
+ @param uuid: The uuid of the characteristic to read, as a string.
+ @param address: The MAC address of the remote device.
+
+ @returns: A b64 encoded version of a byte array containing the value
+ if the uuid/address is in the object tree.
+ None if the uuid/address was not found in the object tree, or
+ if a DBus exception was raised by the read operation.
+
+ """
+ char_obj = self._get_char_object(uuid, address)
+ if char_obj is None:
+ return None
+ value = char_obj.ReadValue(dbus.Dictionary())
+ return _dbus_byte_array_to_b64_string(value)
+
+
+ @xmlrpc_server.dbus_safe(None)
+ def write_characteristic(self, uuid, address, value):
+ """Performs a write operation on a gatt characteristic.
+
+ Writes to a GATT characteristic on a remote device. Base64 endcoding is
+ used for compatibility with the XML RPC interface.
+
+ @param uuid: The uuid of the characteristic to write to, as a string.
+ @param address: The MAC address of the remote device, as a string.
+ @param value: A byte array containing the data to write.
+
+ @returns: True if the write operation does not raise an exception.
+ None if the uuid/address was not found in the object tree, or
+ if a DBus exception was raised by the write operation.
+
+ """
+ char_obj = self._get_char_object(uuid, address)
+ if char_obj is None:
+ return None
+ dbus_value = _b64_string_to_dbus_byte_array(value)
+ char_obj.WriteValue(dbus_value, dbus.Dictionary())
+ return True
+
+
+ @xmlrpc_server.dbus_safe(False)
+ def is_characteristic_path_resolved(self, uuid, address):
+ """Checks whether a characteristic is in the object tree.
+
+ Checks whether a characteristic is curently found in the object tree.
+
+ @param uuid: The uuid of the characteristic to search for.
+ @param address: The MAC address of the device on which to search for
+ the characteristic.
+
+ @returns: True if the characteristic is found.
+ False if the characteristic path is not found.
+
+ """
+ return bool(self.get_characteristic_map(address).get(uuid))
+
+
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
handler = logging.handlers.SysLogHandler(address='/dev/log')
diff --git a/server/cros/bluetooth/bluetooth_device.py b/server/cros/bluetooth/bluetooth_device.py
index b0d9a5f..ae18085 100644
--- a/server/cros/bluetooth/bluetooth_device.py
+++ b/server/cros/bluetooth/bluetooth_device.py
@@ -2,6 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import base64
import json
from autotest_lib.client.cros import constants
@@ -456,6 +457,17 @@
return self._proxy.device_is_paired(address)
+ def device_services_resolved(self, address):
+ """Checks if services are resolved for a device.
+
+ @param address: address of the device.
+
+ @returns: True if services are resolved. False otherwise.
+
+ """
+ return self._proxy.device_services_resolved(address)
+
+
def set_trusted(self, address, trusted=True):
"""Set the device trusted.
@@ -623,6 +635,59 @@
return self._proxy.reset_advertising()
+ def read_characteristic(self, uuid, address):
+ """Reads the value of a gatt characteristic.
+
+ Reads the current value of a gatt characteristic.
+
+ @param uuid: The uuid of the characteristic to read, as a string.
+ @param address: The MAC address of the remote device.
+
+ @returns: A byte array containing the value of the if the uuid/address
+ was found in the object tree.
+ None if the uuid/address was not found in the object tree, or
+ if a DBus exception was raised by the read operation.
+
+ """
+ value = self._proxy.read_characteristic(uuid, address)
+ if value is None:
+ return None
+ return bytearray(base64.standard_b64decode(value))
+
+
+ def write_characteristic(self, uuid, address, bytes_to_write):
+ """Performs a write operation on a gatt characteristic.
+
+ Writes to a GATT characteristic on a remote device.
+
+ @param uuid: The uuid of the characteristic to write to, as a string.
+ @param address: The MAC address of the remote device, as a string.
+ @param bytes_to_write: A byte array containing the data to write.
+
+ @returns: True if the write operation does not raise an exception.
+ None if the uuid/address was not found in the object tree, or
+ if a DBus exception was raised by the write operation.
+
+ """
+ return self._proxy.write_characteristic(
+ uuid, address, base64.standard_b64encode(bytes_to_write))
+
+
+ def is_characteristic_path_resolved(self, uuid, address):
+ """Checks whether a characteristic is in the object tree.
+
+ Checks whether a characteristic is curently found in the object tree.
+
+ @param uuid: The uuid of the characteristic to search for.
+ @param address: The MAC address of the device on which to search for
+ the characteristic.
+
+ @returns: True if the characteristic is found, False otherwise.
+
+ """
+ return self._proxy.is_characteristic_path_resolved(uuid, address)
+
+
def copy_logs(self, destination):
"""Copy the logs generated by this device to a given location.
@@ -632,11 +697,22 @@
self.host.collect_logs(self.XMLRPC_LOG_PATH, destination)
- def close(self):
- """Tear down state associated with the client."""
+ def close(self, close_host=True):
+ """Tear down state associated with the client.
+
+ @param close_host: If True, shut down the xml rpc server by closing the
+ underlying host object (which also shuts down all other xml rpc
+ servers running on the DUT). Otherwise, only shut down the
+ bluetooth device xml rpc server, which can be desirable if the host
+ object and/or other xml rpc servers need to be used afterwards.
+ """
# Turn off the discoverable flag since it may affect future tests.
self._proxy.set_discoverable(False)
# Leave the adapter powered off, but don't do a full reset.
self._proxy.set_powered(False)
# This kills the RPC server.
- self.host.close()
+ if close_host:
+ self.host.close()
+ else:
+ self.host.rpc_server_tracker.disconnect(
+ constants.BLUETOOTH_DEVICE_XMLRPC_SERVER_PORT)