1#!/usr/bin/env python3 2# 3# Scrape SDP UUIDs from Bluetooth SIG page 4# Copyright 2017 BlueKitchen GmbH 5# 6 7from lxml import html 8import datetime 9import requests 10import sys 11import os 12import codecs 13import re 14 15headers = {'user-agent': 'curl/7.63.0'} 16 17program_info = ''' 18BTstack SDP UUID Scraper for BTstack 19Copyright 2017, BlueKitchen GmbH 20''' 21 22header = '''/** 23 * bluetooth_sdp.h generated from Bluetooth SIG website for BTstack by tool/bluetooth_sdp.py 24 * {page} 25 * {datetime} 26 */ 27 28#ifndef BLUETOOTH_SDP_H 29#define BLUETOOTH_SDP_H 30 31''' 32 33trailer = ''' 34#endif 35''' 36 37defines = [] 38 39# Convert CamelCase to snake_case from http://stackoverflow.com/a/1176023 40def camel_to_underscore(name): 41 s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 42 return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).upper() 43 44def create_pretty_define(name): 45 name = name.lstrip() 46 to_delete = [ '(FTP v1.2 and later)', '(Deprecated)', '(FTP v1.2 and later)', '(GOEP v2.0 and later)', 47 '(BIP v1.1 and later)', '(MAP v1.2 and later)', '(OPP v1.2 and later)', '(Not used in PAN v1.0)', '(PBAP v1.2 and later)'] 48 for item in to_delete: 49 name = name.replace(item, '') 50 name = name.rstrip() 51 name = name.replace(' - ', '_') 52 name = name.replace(' ', '_') 53 name = name.replace('/','') 54 name = name.replace('(','_') 55 name = name.replace(')','') 56 name = name.replace('-','_') 57 name = name.replace('.','_') 58 name = name.replace('PnP', 'PNP') 59 name = name.replace('IPv', 'IPV') 60 name = name.replace('ServiceDiscoveryServerServiceClassID', 'ServiceDiscoveryServer') 61 name = name.replace('BrowseGroupDescriptorServiceClassID', 'BrowseGroupDescriptor') 62 name = name.replace('&','and') 63 name = name.replace('__','_') 64 return camel_to_underscore(name).replace('__','_').replace('3_D','3D').replace('L2_CAP','L2CAP') 65 66def remove_newlines(remark): 67 return " ".join(remark.split()) 68 69def process_rows(fout, rows, pattern): 70 for row in rows: 71 columns = row.getchildren() 72 name = columns[0].text_content().replace(u'\u2013','').replace(u'\u200b','') 73 value = columns[1].text_content().replace(u'\u200b','') 74 remark = '' 75 if (len(columns) > 2): 76 remark = columns[2].text_content().replace(u'\u200b','') 77 # skip tbody headers 78 if name in ["Protocol Name", "Service Class Name", "Attribute Name", "UUID Name", 79 "Reserved", 'Reserved for HID Attributes', 'Available for HID Language Strings']: 80 continue 81 # skip tbody footers 82 if value.startswith('(Max value '): 83 continue 84 name = create_pretty_define(name) 85 print(name) 86 if name.endswith("DISPLAY"): 87 for c in name: 88 print("ord %c = 0x%x" % (c, ord(c))) 89 90 # skip duplicate attributes 91 if name in defines: 92 continue 93 value = remove_newlines(value) 94 remark = remove_newlines(remark) 95 fout.write(pattern % (name, value, remark)) 96 defines.append(name) 97 98def scrape_page(fout, url): 99 print("Parsing %s" % url) 100 fout.write(header.format(page=url.replace('https://',''),datetime=str(datetime.datetime.now()))) 101 102 # get from web 103 r = requests.get(url, headers=headers) 104 content = r.text 105 106 # test: fetch from local file 'service-discovery.html' 107 # f = codecs.open("service-discovery.html", "r", "utf-8") 108 # content = f.read(); 109 110 tree = html.fromstring(content) 111 112 # Protocol Identifiers 113 fout.write('/**\n') 114 fout.write(' * Protocol Identifiers\n') 115 fout.write(' */\n') 116 rows = tree.xpath("//table[2]/tbody/tr") 117 process_rows(fout, rows, '#define BLUETOOTH_PROTOCOL_%-55s %s // %s\n') 118 fout.write('\n') 119 120 # Service Classes 121 fout.write('/**\n') 122 fout.write(' * Service Classes\n') 123 fout.write(' */\n') 124 rows = tree.xpath("//table[3]/tr") 125 process_rows(fout, rows, '#define BLUETOOTH_SERVICE_CLASS_%-50s %s // %s\n') 126 fout.write('\n') 127 128 # Attributes 129 fout.write('/**\n') 130 fout.write(' * Attributes\n') 131 fout.write(' */\n') 132 table_names = [ 133 # 'Base Universally Unique Identifier (UUID)', 134 'Browse Group Identifiers', 135 'Attribute Identifiers', 136 # 'Audio/Video Remote Control Profile (AVRCP)', 137 'Basic Imaging Profile (BIP)', 138 'Basic Printing Profile (BPP)', 139 'Bluetooth Core Specification: Universal Attributes', 140 'Bluetooth Core Specification: Service Discovery Service', 141 # 'Bluetooth Core Specification: Browse Group Descriptor Service', 142 # 'Cordless Telephony Profile [DEPRECATED]', 143 'Device Identification Profile', 144 # 'Fax Profile [DEPRECATED]', 145 'File Transfer Profile', 146 'Generic Object Exchange Profile', 147 # 'Global Navigation Satellite System Profile (GNSS)', -- note: SupportedFeatures, but different UUID 148 'Hands-Free Profile', 149 'Hardcopy Replacement Profile ', 150 'Headset Profile', 151 'Health Device Profile', 152 'Human Interface Device Profile', 153 # 'Interoperability Requirements for Bluetooth technology as a WAP Bearer [DEPRECATED]', 154 'Message Access Profile', 155 'Object Push Profile', 156 'Personal Area Networking Profile', 157 'Phone Book Access Profile', 158 'Synchronization Profile', 159 # 'Attribute ID Offsets for Strings', 160 # 'Protocol Parameters', 161 'Multi-Profile', 162 'Calendar Tasks and Notes', 163 ] 164 for table_name in table_names: 165 rows = tree.xpath("//table[preceding-sibling::h3 = '" + table_name +"']/tr") 166 process_rows(fout, rows, '#define BLUETOOTH_ATTRIBUTE_%-54s %s // %s\n') 167 # scrape_attributes(fout, tree, table_name) 168 # see above 169 fout.write('#define BLUETOOTH_ATTRIBUTE_GNSS_SUPPORTED_FEATURES 0x0200\n'); 170 171 172 173btstack_root = os.path.abspath(os.path.dirname(sys.argv[0]) + '/..') 174gen_path = btstack_root + '/src/bluetooth_sdp.h' 175 176print(program_info) 177 178with open(gen_path, 'wt') as fout: 179 scrape_page(fout, 'https://www.bluetooth.com/specifications/assigned-numbers/service-discovery') 180 fout.write(trailer) 181 182print('Scraping successful!\n') 183