# 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)