576 lines
18 KiB
Python
576 lines
18 KiB
Python
# Copyright 2019 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""Server side bluetooth GATT client helper class for testing"""
|
|
|
|
import base64
|
|
import json
|
|
|
|
|
|
class GATT_ClientFacade(object):
|
|
"""A wrapper for getting GATT application from GATT server"""
|
|
|
|
def __init__(self, bluetooth_facade):
|
|
"""Initialize a GATT_ClientFacade
|
|
|
|
@param bluetooth_facade: facade to communicate with adapter in DUT
|
|
|
|
"""
|
|
self.bluetooth_facade = bluetooth_facade
|
|
|
|
|
|
def browse(self, address):
|
|
"""Browse the application on GATT server
|
|
|
|
@param address: a string of MAC address of the GATT server device
|
|
|
|
@return: GATT_Application object
|
|
|
|
"""
|
|
attr_map_json = json.loads(self.bluetooth_facade.\
|
|
get_gatt_attributes_map(address))
|
|
application = GATT_Application()
|
|
application.browse(attr_map_json, self.bluetooth_facade)
|
|
|
|
return application
|
|
|
|
|
|
class GATT_Application(object):
|
|
"""A GATT client application class"""
|
|
|
|
def __init__(self):
|
|
"""Initialize a GATT Application"""
|
|
self.services = dict()
|
|
|
|
|
|
def browse(self, attr_map_json, bluetooth_facade):
|
|
"""Browse the application on GATT server
|
|
|
|
@param attr_map_json: a json object returned by
|
|
bluetooth_device_xmlrpc_server
|
|
|
|
@bluetooth_facade: facade to communicate with adapter in DUT
|
|
|
|
"""
|
|
servs_json = attr_map_json['services']
|
|
for uuid in servs_json:
|
|
path = servs_json[uuid]['path']
|
|
service_obj = GATT_Service(uuid, path, bluetooth_facade)
|
|
service_obj.read_properties()
|
|
self.add_service(service_obj)
|
|
|
|
chrcs_json = servs_json[uuid]['characteristics']
|
|
for uuid in chrcs_json:
|
|
path = chrcs_json[uuid]['path']
|
|
chrc_obj = GATT_Characteristic(uuid, path, bluetooth_facade)
|
|
chrc_obj.read_properties()
|
|
service_obj.add_characteristic(chrc_obj)
|
|
|
|
descs_json = chrcs_json[uuid]['descriptors']
|
|
for uuid in descs_json:
|
|
path = descs_json[uuid]['path']
|
|
desc_obj = GATT_Descriptor(uuid, path, bluetooth_facade)
|
|
desc_obj.read_properties()
|
|
chrc_obj.add_descriptor(desc_obj)
|
|
|
|
|
|
def find_by_uuid(self, uuid):
|
|
"""Find attribute under this application by specifying UUID
|
|
|
|
@param uuid: string of UUID
|
|
|
|
@return: Attribute object if found,
|
|
none otherwise
|
|
"""
|
|
for serv_uuid, serv in self.services.items():
|
|
found = serv.find_by_uuid(uuid)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
|
|
def add_service(self, service):
|
|
"""Add a service into this application"""
|
|
self.services[service.uuid] = service
|
|
|
|
|
|
@staticmethod
|
|
def diff(appl_a, appl_b):
|
|
"""Compare two Applications, and return their difference
|
|
|
|
@param appl_a: the first application which is going to be compared
|
|
|
|
@param appl_b: the second application which is going to be compared
|
|
|
|
@return: a list of string, each describes one difference
|
|
|
|
"""
|
|
result = []
|
|
|
|
uuids_a = set(appl_a.services.keys())
|
|
uuids_b = set(appl_b.services.keys())
|
|
uuids = uuids_a.union(uuids_b)
|
|
|
|
for uuid in uuids:
|
|
serv_a = appl_a.services.get(uuid, None)
|
|
serv_b = appl_b.services.get(uuid, None)
|
|
|
|
if not serv_a or not serv_b:
|
|
result.append("Service %s is not included in both Applications:"
|
|
"%s vs %s" % (uuid, bool(serv_a), bool(serv_b)))
|
|
else:
|
|
result.extend(GATT_Service.diff(serv_a, serv_b))
|
|
return result
|
|
|
|
|
|
class GATT_Service(object):
|
|
"""GATT client service class"""
|
|
PROPERTIES = ['UUID', 'Primary', 'Device', 'Includes']
|
|
|
|
|
|
def __init__(self, uuid, object_path, bluetooth_facade):
|
|
"""Initialize a GATT service object
|
|
|
|
@param uuid: string of UUID
|
|
|
|
@param object_path: object path of this service
|
|
|
|
@param bluetooth_facade: facade to communicate with adapter in DUT
|
|
|
|
"""
|
|
self.uuid = uuid
|
|
self.object_path = object_path
|
|
self.bluetooth_facade = bluetooth_facade
|
|
self.properties = dict()
|
|
self.characteristics = dict()
|
|
|
|
|
|
def add_characteristic(self, chrc_obj):
|
|
"""Add a characteristic attribute into service
|
|
|
|
@param chrc_obj: a characteristic object
|
|
|
|
"""
|
|
self.characteristics[chrc_obj.uuid] = chrc_obj
|
|
|
|
|
|
def read_properties(self):
|
|
"""Read all properties in this service"""
|
|
for prop_name in self.PROPERTIES:
|
|
self.properties[prop_name] = self.read_property(prop_name)
|
|
return self.properties
|
|
|
|
|
|
def read_property(self, property_name):
|
|
"""Read a property in this service
|
|
|
|
@param property_name: string of the name of the property
|
|
|
|
@return: the value of the property
|
|
|
|
"""
|
|
return self.bluetooth_facade.get_gatt_service_property(
|
|
self.object_path, property_name)
|
|
|
|
def find_by_uuid(self, uuid):
|
|
"""Find attribute under this service by specifying UUID
|
|
|
|
@param uuid: string of UUID
|
|
|
|
@return: Attribute object if found,
|
|
none otherwise
|
|
|
|
"""
|
|
if self.uuid == uuid:
|
|
return self
|
|
|
|
for chrc_uuid, chrc in self.characteristics.items():
|
|
found = chrc.find_by_uuid(uuid)
|
|
if found:
|
|
return found
|
|
return None
|
|
|
|
|
|
@staticmethod
|
|
def diff(serv_a, serv_b):
|
|
"""Compare two Services, and return their difference
|
|
|
|
@param serv_a: the first service which is going to be compared
|
|
|
|
@param serv_b: the second service which is going to be compared
|
|
|
|
@return: a list of string, each describes one difference
|
|
|
|
"""
|
|
result = []
|
|
|
|
for prop_name in GATT_Service.PROPERTIES:
|
|
if serv_a.properties[prop_name] != serv_b.properties[prop_name]:
|
|
result.append("Service %s is different in %s: %s vs %s" %
|
|
(serv_a.uuid, prop_name,
|
|
serv_a.properties[prop_name],
|
|
serv_b.properties[prop_name]))
|
|
|
|
uuids_a = set(serv_a.characteristics.keys())
|
|
uuids_b = set(serv_b.characteristics.keys())
|
|
uuids = uuids_a.union(uuids_b)
|
|
|
|
for uuid in uuids:
|
|
chrc_a = serv_a.characteristics.get(uuid, None)
|
|
chrc_b = serv_b.characteristics.get(uuid, None)
|
|
|
|
if not chrc_a or not chrc_b:
|
|
result.append("Characteristic %s is not included in both "
|
|
"Services: %s vs %s" % (uuid, bool(chrc_a),
|
|
bool(chrc_b)))
|
|
else:
|
|
result.extend(GATT_Characteristic.diff(chrc_a, chrc_b))
|
|
return result
|
|
|
|
|
|
class GATT_Characteristic(object):
|
|
"""GATT client characteristic class"""
|
|
|
|
PROPERTIES = ['UUID', 'Service', 'Value', 'Notifying', 'Flags']
|
|
|
|
|
|
def __init__(self, uuid, object_path, bluetooth_facade):
|
|
"""Initialize a GATT characteristic object
|
|
|
|
@param uuid: string of UUID
|
|
|
|
@param object_path: object path of this characteristic
|
|
|
|
@param bluetooth_facade: facade to communicate with adapter in DUT
|
|
|
|
"""
|
|
self.uuid = uuid
|
|
self.object_path = object_path
|
|
self.bluetooth_facade = bluetooth_facade
|
|
self.properties = dict()
|
|
self.descriptors = dict()
|
|
|
|
|
|
def add_descriptor(self, desc_obj):
|
|
"""Add a characteristic attribute into service
|
|
|
|
@param desc_obj: a descriptor object
|
|
|
|
"""
|
|
self.descriptors[desc_obj.uuid] = desc_obj
|
|
|
|
|
|
def read_properties(self):
|
|
"""Read all properties in this characteristic"""
|
|
for prop_name in self.PROPERTIES:
|
|
self.properties[prop_name] = self.read_property(prop_name)
|
|
return self.properties
|
|
|
|
|
|
def read_property(self, property_name):
|
|
"""Read a property in this characteristic
|
|
|
|
@param property_name: string of the name of the property
|
|
|
|
@return: the value of the property
|
|
|
|
"""
|
|
return self.bluetooth_facade.get_gatt_characteristic_property(
|
|
self.object_path, property_name)
|
|
|
|
|
|
def find_by_uuid(self, uuid):
|
|
"""Find attribute under this characteristic by specifying UUID
|
|
|
|
@param uuid: string of UUID
|
|
|
|
@return: Attribute object if found,
|
|
none otherwise
|
|
|
|
"""
|
|
if self.uuid == uuid:
|
|
return self
|
|
|
|
for desc_uuid, desc in self.descriptors.items():
|
|
if desc_uuid == uuid:
|
|
return desc
|
|
return None
|
|
|
|
|
|
def read_value(self):
|
|
"""Perform ReadValue in DUT and store it in property 'Value'
|
|
|
|
@return: bytearray of the value
|
|
|
|
"""
|
|
value = self.bluetooth_facade.gatt_characteristic_read_value(
|
|
self.uuid, self.object_path)
|
|
self.properties['Value'] = bytearray(base64.standard_b64decode(value))
|
|
return self.properties['Value']
|
|
|
|
|
|
@staticmethod
|
|
def diff(chrc_a, chrc_b):
|
|
"""Compare two Characteristics, and return their difference
|
|
|
|
@param serv_a: the first service which is going to be compared
|
|
|
|
@param serv_b: the second service which is going to be compared
|
|
|
|
@return: a list of string, each describes one difference
|
|
|
|
"""
|
|
result = []
|
|
|
|
for prop_name in GATT_Characteristic.PROPERTIES:
|
|
if chrc_a.properties[prop_name] != chrc_b.properties[prop_name]:
|
|
result.append("Characteristic %s is different in %s: %s vs %s"
|
|
% (chrc_a.uuid, prop_name,
|
|
chrc_a.properties[prop_name],
|
|
chrc_b.properties[prop_name]))
|
|
|
|
uuids_a = set(chrc_a.descriptors.keys())
|
|
uuids_b = set(chrc_b.descriptors.keys())
|
|
uuids = uuids_a.union(uuids_b)
|
|
|
|
for uuid in uuids:
|
|
desc_a = chrc_a.descriptors.get(uuid, None)
|
|
desc_b = chrc_b.descriptors.get(uuid, None)
|
|
|
|
if not desc_a or not desc_b:
|
|
result.append("Descriptor %s is not included in both"
|
|
"Characteristic: %s vs %s" % (uuid, bool(desc_a),
|
|
bool(desc_b)))
|
|
else:
|
|
result.extend(GATT_Descriptor.diff(desc_a, desc_b))
|
|
return result
|
|
|
|
|
|
class GATT_Descriptor(object):
|
|
"""GATT client descriptor class"""
|
|
|
|
PROPERTIES = ['UUID', 'Characteristic', 'Value', 'Flags']
|
|
|
|
def __init__(self, uuid, object_path, bluetooth_facade):
|
|
"""Initialize a GATT descriptor object
|
|
|
|
@param uuid: string of UUID
|
|
|
|
@param object_path: object path of this descriptor
|
|
|
|
@param bluetooth_facade: facade to communicate with adapter in DUT
|
|
|
|
"""
|
|
self.uuid = uuid
|
|
self.object_path = object_path
|
|
self.bluetooth_facade = bluetooth_facade
|
|
self.properties = dict()
|
|
|
|
|
|
def read_properties(self):
|
|
"""Read all properties in this characteristic"""
|
|
for prop_name in self.PROPERTIES:
|
|
self.properties[prop_name] = self.read_property(prop_name)
|
|
return self.properties
|
|
|
|
|
|
def read_property(self, property_name):
|
|
"""Read a property in this characteristic
|
|
|
|
@param property_name: string of the name of the property
|
|
|
|
@return: the value of the property
|
|
|
|
"""
|
|
return self.bluetooth_facade.get_gatt_descriptor_property(
|
|
self.object_path, property_name)
|
|
|
|
|
|
def read_value(self):
|
|
"""Perform ReadValue in DUT and store it in property 'Value'
|
|
|
|
@return: bytearray of the value
|
|
|
|
"""
|
|
value = self.bluetooth_facade.gatt_descriptor_read_value(
|
|
self.uuid, self.object_path)
|
|
self.properties['Value'] = bytearray(base64.standard_b64decode(value))
|
|
|
|
return self.properties['Value']
|
|
|
|
|
|
@staticmethod
|
|
def diff(desc_a, desc_b):
|
|
"""Compare two Descriptors, and return their difference
|
|
|
|
@param serv_a: the first service which is going to be compared
|
|
|
|
@param serv_b: the second service which is going to be compared
|
|
|
|
@return: a list of string, each describes one difference
|
|
|
|
"""
|
|
result = []
|
|
|
|
for prop_name in desc_a.properties.keys():
|
|
if desc_a.properties[prop_name] != desc_b.properties[prop_name]:
|
|
result.append("Descriptor %s is different in %s: %s vs %s" %
|
|
(desc_a.uuid, prop_name,
|
|
desc_a.properties[prop_name],
|
|
desc_b.properties[prop_name]))
|
|
|
|
return result
|
|
|
|
|
|
def UUID_Short2Full(uuid):
|
|
"""Transform 2 bytes uuid string to 16 bytes
|
|
|
|
@param uuid: 2 bytes shortened UUID string in hex
|
|
|
|
@return: full uuid string
|
|
"""
|
|
uuid_template = '0000%s-0000-1000-8000-00805f9b34fb'
|
|
return uuid_template % uuid
|
|
|
|
|
|
class GATT_HIDApplication(GATT_Application):
|
|
"""Default HID Application on Raspberry Pi GATT server
|
|
"""
|
|
|
|
BatteryServiceUUID = UUID_Short2Full('180f')
|
|
BatteryLevelUUID = UUID_Short2Full('2a19')
|
|
CliChrcConfigUUID = UUID_Short2Full('2902')
|
|
GenericAttributeProfileUUID = UUID_Short2Full('1801')
|
|
ServiceChangedUUID = UUID_Short2Full('2a05')
|
|
DeviceInfoUUID = UUID_Short2Full('180a')
|
|
ManufacturerNameStrUUID = UUID_Short2Full('2a29')
|
|
PnPIDUUID = UUID_Short2Full('2a50')
|
|
GenericAccessProfileUUID = UUID_Short2Full('1800')
|
|
DeviceNameUUID = UUID_Short2Full('2a00')
|
|
AppearanceUUID = UUID_Short2Full('2a01')
|
|
|
|
|
|
def __init__(self):
|
|
"""
|
|
"""
|
|
GATT_Application.__init__(self)
|
|
BatteryService = GATT_Service(self.BatteryServiceUUID, None, None)
|
|
BatteryService.properties = {
|
|
'UUID': BatteryService.uuid,
|
|
'Primary': True,
|
|
'Device': None,
|
|
'Includes': []
|
|
}
|
|
self.add_service(BatteryService)
|
|
|
|
BatteryLevel = GATT_Characteristic(self.BatteryLevelUUID, None, None)
|
|
BatteryLevel.properties = {
|
|
'UUID': BatteryLevel.uuid,
|
|
'Service': None,
|
|
'Value': [],
|
|
'Notifying': False,
|
|
'Flags': ['read', 'notify']
|
|
}
|
|
BatteryService.add_characteristic(BatteryLevel)
|
|
|
|
CliChrcConfig = GATT_Descriptor(self.CliChrcConfigUUID, None, None)
|
|
CliChrcConfig.properties = {
|
|
'UUID': CliChrcConfig.uuid,
|
|
'Characteristic': None,
|
|
'Value': [],
|
|
'Flags': None
|
|
}
|
|
|
|
BatteryLevel.add_descriptor(CliChrcConfig)
|
|
|
|
GenericAttributeProfile = GATT_Service(self.GenericAttributeProfileUUID,
|
|
None, None)
|
|
GenericAttributeProfile.properties = {
|
|
'UUID': GenericAttributeProfile.uuid,
|
|
'Primary': True,
|
|
'Device': None,
|
|
'Includes': []
|
|
}
|
|
self.add_service(GenericAttributeProfile)
|
|
|
|
ServiceChanged = GATT_Characteristic(self.ServiceChangedUUID, None,
|
|
None)
|
|
ServiceChanged.properties = {
|
|
'UUID': ServiceChanged.uuid,
|
|
'Service': None,
|
|
'Value': [],
|
|
'Notifying': False,
|
|
'Flags': ['indicate']
|
|
}
|
|
GenericAttributeProfile.add_characteristic(ServiceChanged)
|
|
|
|
CliChrcConfig = GATT_Descriptor(self.CliChrcConfigUUID, None, None)
|
|
CliChrcConfig.properties = {
|
|
'UUID': CliChrcConfig.uuid,
|
|
'Characteristic': None,
|
|
'Value': [],
|
|
'Flags': None
|
|
}
|
|
ServiceChanged.add_descriptor(CliChrcConfig)
|
|
|
|
DeviceInfo = GATT_Service(self.DeviceInfoUUID, None, None)
|
|
DeviceInfo.properties = {
|
|
'UUID': DeviceInfo.uuid,
|
|
'Primary': True,
|
|
'Device': None,
|
|
'Includes': []
|
|
}
|
|
self.add_service(DeviceInfo)
|
|
|
|
ManufacturerNameStr = GATT_Characteristic(self.ManufacturerNameStrUUID,
|
|
None, None)
|
|
ManufacturerNameStr.properties = {
|
|
'UUID': ManufacturerNameStr.uuid,
|
|
'Service': None,
|
|
'Value': [],
|
|
'Notifying': None,
|
|
'Flags': ['read']
|
|
}
|
|
DeviceInfo.add_characteristic(ManufacturerNameStr)
|
|
|
|
PnPID = GATT_Characteristic(self.PnPIDUUID, None, None)
|
|
PnPID.properties = {
|
|
'UUID': PnPID.uuid,
|
|
'Service': None,
|
|
'Value': [],
|
|
'Notifying': None,
|
|
'Flags': ['read']
|
|
}
|
|
DeviceInfo.add_characteristic(PnPID)
|
|
|
|
GenericAccessProfile = GATT_Service(self.GenericAccessProfileUUID,
|
|
None, None)
|
|
GenericAccessProfile.properties = {
|
|
'UUID': GenericAccessProfile.uuid,
|
|
'Primary': True,
|
|
'Device': None,
|
|
'Includes': []
|
|
}
|
|
self.add_service(GenericAccessProfile)
|
|
|
|
DeviceName = GATT_Characteristic(self.DeviceNameUUID, None, None)
|
|
DeviceName.properties = {
|
|
'UUID': DeviceName.uuid,
|
|
'Service': None,
|
|
'Value': [],
|
|
'Notifying': None,
|
|
'Flags': ['read']
|
|
}
|
|
GenericAccessProfile.add_characteristic(DeviceName)
|
|
|
|
Appearance = GATT_Characteristic(self.AppearanceUUID, None, None)
|
|
Appearance.properties = {
|
|
'UUID': Appearance.uuid,
|
|
'Service': None,
|
|
'Value': [],
|
|
'Notifying': None,
|
|
'Flags': ['read']
|
|
}
|
|
GenericAccessProfile.add_characteristic(Appearance)
|