1#!/usr/bin/env python3 2# 3# BLE GATT configuration generator for use with BTstack 4# Copyright 2019 BlueKitchen GmbH 5# 6# Format of input file: 7# PRIMARY_SERVICE, SERVICE_UUID 8# CHARACTERISTIC, ATTRIBUTE_TYPE_UUID, [READ | WRITE | DYNAMIC], VALUE 9 10# dependencies: 11# - pip3 install pycryptodomex 12# alternatively, the pycryptodome package can be used instead 13# - pip3 install pycryptodome 14 15import codecs 16import csv 17import io 18import os 19import re 20import string 21import sys 22import argparse 23import tempfile 24 25have_crypto = True 26# try to import PyCryptodome independent from PyCrypto 27try: 28 from Cryptodome.Cipher import AES 29 from Cryptodome.Hash import CMAC 30except ImportError: 31 # fallback: try to import PyCryptodome as (an almost drop-in) replacement for the PyCrypto library 32 try: 33 from Crypto.Cipher import AES 34 from Crypto.Hash import CMAC 35 except ImportError: 36 have_crypto = False 37 print("\n[!] PyCryptodome required to calculate GATT Database Hash but not installed (using random value instead)") 38 print("[!] Please install PyCryptodome, e.g. 'pip3 install pycryptodomex' or 'pip3 install pycryptodome'\n") 39 40header = ''' 41// {0} generated from {1} for BTstack 42// it needs to be regenerated when the .gatt file is updated. 43 44// To generate {0}: 45// {2} {1} {0} 46 47// att db format version 1 48 49// binary attribute representation: 50// - size in bytes (16), flags(16), handle (16), uuid (16/128), value(...) 51 52#include <stdint.h> 53 54// Reference: https://en.cppreference.com/w/cpp/feature_test 55#if __cplusplus >= 200704L 56constexpr 57#endif 58const uint8_t profile_data[] = 59''' 60 61print(''' 62BLE configuration generator for use with BTstack 63Copyright 2018 BlueKitchen GmbH 64''') 65 66assigned_uuids = { 67 'GAP_SERVICE' : 0x1800, 68 'GATT_SERVICE' : 0x1801, 69 'GAP_DEVICE_NAME' : 0x2a00, 70 'GAP_APPEARANCE' : 0x2a01, 71 'GAP_PERIPHERAL_PRIVACY_FLAG' : 0x2A02, 72 'GAP_RECONNECTION_ADDRESS' : 0x2A03, 73 'GAP_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS' : 0x2A04, 74 'GAP_CENTRAL_ADDRESS_RESOLUTION' : 0x2aa6, 75 'GAP_RESOLVABLE_PRIVATE_ADDRESS_ONLY' : 0x2AC9, 76 'GAP_ENCRYPTED_DATA_KEY_MATERIAL' : 0x2B88, 77 'GAP_LE_GATT_SECURITY_LEVELS' : 0x2BF5, 78 'GATT_SERVICE_CHANGED' : 0x2a05, 79 'GATT_CLIENT_SUPPORTED_FEATURES' : 0x2b29, 80 'GATT_SERVER_SUPPORTED_FEATURES' : 0x2b3a, 81 'GATT_DATABASE_HASH' : 0x2b2a 82} 83 84security_permsission = ['ANYBODY','ENCRYPTED', 'AUTHENTICATED', 'AUTHORIZED', 'AUTHENTICATED_SC'] 85 86property_flags = { 87 # GATT Characteristic Properties 88 'BROADCAST' : 0x01, 89 'READ' : 0x02, 90 'WRITE_WITHOUT_RESPONSE' : 0x04, 91 'WRITE' : 0x08, 92 'NOTIFY': 0x10, 93 'INDICATE' : 0x20, 94 'AUTHENTICATED_SIGNED_WRITE' : 0x40, 95 'EXTENDED_PROPERTIES' : 0x80, 96 # custom BTstack extension 97 'DYNAMIC': 0x100, 98 'LONG_UUID': 0x200, 99 100 # read permissions 101 'READ_PERMISSION_BIT_0': 0x400, 102 'READ_PERMISSION_BIT_1': 0x800, 103 104 # 105 'ENCRYPTION_KEY_SIZE_7': 0x6000, 106 'ENCRYPTION_KEY_SIZE_8': 0x7000, 107 'ENCRYPTION_KEY_SIZE_9': 0x8000, 108 'ENCRYPTION_KEY_SIZE_10': 0x9000, 109 'ENCRYPTION_KEY_SIZE_11': 0xa000, 110 'ENCRYPTION_KEY_SIZE_12': 0xb000, 111 'ENCRYPTION_KEY_SIZE_13': 0xc000, 112 'ENCRYPTION_KEY_SIZE_14': 0xd000, 113 'ENCRYPTION_KEY_SIZE_15': 0xe000, 114 'ENCRYPTION_KEY_SIZE_16': 0xf000, 115 'ENCRYPTION_KEY_SIZE_MASK': 0xf000, 116 117 # only used by gatt compiler >= 0xffff 118 # Extended Properties 119 'RELIABLE_WRITE': 0x00010000, 120 'AUTHENTICATION_REQUIRED': 0x00020000, 121 'AUTHORIZATION_REQUIRED': 0x00040000, 122 'READ_ANYBODY': 0x00080000, 123 'READ_ENCRYPTED': 0x00100000, 124 'READ_AUTHENTICATED': 0x00200000, 125 'READ_AUTHENTICATED_SC': 0x00400000, 126 'READ_AUTHORIZED': 0x00800000, 127 'WRITE_ANYBODY': 0x01000000, 128 'WRITE_ENCRYPTED': 0x02000000, 129 'WRITE_AUTHENTICATED': 0x04000000, 130 'WRITE_AUTHENTICATED_SC': 0x08000000, 131 'WRITE_AUTHORIZED': 0x10000000, 132 133 # Broadcast, Notify, Indicate, Extended Properties are only used to describe a GATT Characteristic, but are free to use with att_db 134 # - write permissions 135 'WRITE_PERMISSION_BIT_0': 0x01, 136 'WRITE_PERMISSION_BIT_1': 0x10, 137 # - SC required 138 'READ_PERMISSION_SC': 0x20, 139 'WRITE_PERMISSION_SC': 0x80, 140} 141 142services = dict() 143characteristic_indices = dict() 144presentation_formats = dict() 145current_service_uuid_string = "" 146current_service_start_handle = 0 147current_characteristic_uuid_string = "" 148defines_for_characteristics = [] 149defines_for_services = [] 150include_paths = [] 151database_hash_message = bytearray() 152service_counter = {} 153 154handle = 1 155total_size = 0 156 157def aes_cmac(key, n): 158 if have_crypto: 159 cobj = CMAC.new(key, ciphermod=AES) 160 cobj.update(n) 161 return cobj.digest() 162 else: 163 # return random value 164 return os.urandom(16) 165 166def read_defines(infile): 167 defines = dict() 168 with open (infile, 'rt') as fin: 169 for line in fin: 170 parts = re.match('#define\s+(\w+)\s+(\w+)',line) 171 if parts and len(parts.groups()) == 2: 172 (key, value) = parts.groups() 173 defines[key] = int(value, 16) 174 return defines 175 176def keyForUUID(uuid): 177 keyUUID = "" 178 for i in uuid: 179 keyUUID += "%02x" % i 180 return keyUUID 181 182def c_string_for_uuid(uuid): 183 return uuid.replace('-', '_') 184 185def twoByteLEFor(value): 186 return [ (value & 0xff), (value >> 8)] 187 188def is_128bit_uuid(text): 189 if re.match("[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}", text): 190 return True 191 return False 192 193def parseUUID128(uuid): 194 parts = re.match("([0-9A-Fa-f]{4})([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})-([0-9A-Fa-f]{4})([0-9A-Fa-f]{4})([0-9A-Fa-f]{4})", uuid) 195 uuid_bytes = [] 196 for i in range(8, 0, -1): 197 uuid_bytes = uuid_bytes + twoByteLEFor(int(parts.group(i),16)) 198 return uuid_bytes 199 200def parseUUID(uuid): 201 if uuid in assigned_uuids: 202 return twoByteLEFor(assigned_uuids[uuid]) 203 uuid_upper = uuid.upper().replace('.','_') 204 if uuid_upper in bluetooth_gatt: 205 return twoByteLEFor(bluetooth_gatt[uuid_upper]) 206 if is_128bit_uuid(uuid): 207 return parseUUID128(uuid) 208 uuidInt = int(uuid, 16) 209 return twoByteLEFor(uuidInt) 210 211def parseProperties(properties): 212 value = 0 213 parts = properties.split("|") 214 for property in parts: 215 property = property.strip() 216 if property in property_flags: 217 value |= property_flags[property] 218 else: 219 print("WARNING: property %s undefined" % (property)) 220 221 return value 222 223def prettyPrintProperties(properties): 224 value = "" 225 parts = properties.split("|") 226 for property in parts: 227 property = property.strip() 228 if property in property_flags: 229 if value != "": 230 value += " | " 231 value += property 232 else: 233 print("WARNING: property %s undefined" % (property)) 234 235 return value 236 237 238def gatt_characteristic_properties(properties): 239 return properties & 0xff 240 241def att_flags(properties): 242 # drop Broadcast (0x01), Notify (0x10), Indicate (0x20), Extended Properties (0x80) - not used for flags 243 properties &= 0xffffff4e 244 245 # rw permissions distinct 246 distinct_permissions_used = properties & ( 247 property_flags['READ_AUTHORIZED'] | 248 property_flags['READ_AUTHENTICATED_SC'] | 249 property_flags['READ_AUTHENTICATED'] | 250 property_flags['READ_ENCRYPTED'] | 251 property_flags['READ_ANYBODY'] | 252 property_flags['WRITE_AUTHORIZED'] | 253 property_flags['WRITE_AUTHENTICATED'] | 254 property_flags['WRITE_AUTHENTICATED_SC'] | 255 property_flags['WRITE_ENCRYPTED'] | 256 property_flags['WRITE_ANYBODY'] 257 ) != 0 258 259 # post process properties 260 encryption_key_size_specified = (properties & property_flags['ENCRYPTION_KEY_SIZE_MASK']) != 0 261 262 # if distinct permissions not used and encyrption key size specified -> set READ/WRITE Encrypted 263 if encryption_key_size_specified and not distinct_permissions_used: 264 properties |= property_flags['READ_ENCRYPTED'] | property_flags['WRITE_ENCRYPTED'] 265 266 # if distinct permissions not used and authentication is requires -> set READ/WRITE Authenticated 267 if properties & property_flags['AUTHENTICATION_REQUIRED'] and not distinct_permissions_used: 268 properties |= property_flags['READ_AUTHENTICATED'] | property_flags['WRITE_AUTHENTICATED'] 269 270 # if distinct permissions not used and authorized is requires -> set READ/WRITE Authorized 271 if properties & property_flags['AUTHORIZATION_REQUIRED'] and not distinct_permissions_used: 272 properties |= property_flags['READ_AUTHORIZED'] | property_flags['WRITE_AUTHORIZED'] 273 274 # determine read/write security requirements 275 read_security_level = 0 276 write_security_level = 0 277 read_requires_sc = False 278 write_requires_sc = False 279 if properties & property_flags['READ_AUTHORIZED']: 280 read_security_level = 3 281 elif properties & property_flags['READ_AUTHENTICATED']: 282 read_security_level = 2 283 elif properties & property_flags['READ_AUTHENTICATED_SC']: 284 read_security_level = 2 285 read_requires_sc = True 286 elif properties & property_flags['READ_ENCRYPTED']: 287 read_security_level = 1 288 if properties & property_flags['WRITE_AUTHORIZED']: 289 write_security_level = 3 290 elif properties & property_flags['WRITE_AUTHENTICATED']: 291 write_security_level = 2 292 elif properties & property_flags['WRITE_AUTHENTICATED_SC']: 293 write_security_level = 2 294 write_requires_sc = True 295 elif properties & property_flags['WRITE_ENCRYPTED']: 296 write_security_level = 1 297 298 # map security requirements to flags 299 if read_security_level & 2: 300 properties |= property_flags['READ_PERMISSION_BIT_1'] 301 if read_security_level & 1: 302 properties |= property_flags['READ_PERMISSION_BIT_0'] 303 if read_requires_sc: 304 properties |= property_flags['READ_PERMISSION_SC'] 305 if write_security_level & 2: 306 properties |= property_flags['WRITE_PERMISSION_BIT_1'] 307 if write_security_level & 1: 308 properties |= property_flags['WRITE_PERMISSION_BIT_0'] 309 if write_requires_sc: 310 properties |= property_flags['WRITE_PERMISSION_SC'] 311 312 return properties 313 314def write_permissions_and_key_size_flags_from_properties(properties): 315 return att_flags(properties) & (property_flags['ENCRYPTION_KEY_SIZE_MASK'] | property_flags['WRITE_PERMISSION_BIT_0'] | property_flags['WRITE_PERMISSION_BIT_1']) 316 317def write_8(fout, value): 318 fout.write( "0x%02x, " % (value & 0xff)) 319 320def write_16(fout, value): 321 fout.write('0x%02x, 0x%02x, ' % (value & 0xff, (value >> 8) & 0xff)) 322 323def write_uuid(fout, uuid): 324 for byte in uuid: 325 fout.write( "0x%02x, " % byte) 326 327def write_string(fout, text): 328 for l in text.lstrip('"').rstrip('"'): 329 write_8(fout, ord(l)) 330 331def write_sequence(fout, text): 332 parts = text.split() 333 for part in parts: 334 fout.write("0x%s, " % (part.strip())) 335 336def write_database_hash(fout): 337 fout.write("THE-DATABASE-HASH") 338 339def write_indent(fout): 340 fout.write(" ") 341 342def read_permissions_from_flags(flags): 343 permissions = 0 344 if flags & property_flags['READ_PERMISSION_BIT_0']: 345 permissions |= 1 346 if flags & property_flags['READ_PERMISSION_BIT_1']: 347 permissions |= 2 348 if flags & property_flags['READ_PERMISSION_SC'] and permissions == 2: 349 permissions = 4 350 return permissions 351 352def write_permissions_from_flags(flags): 353 permissions = 0 354 if flags & property_flags['WRITE_PERMISSION_BIT_0']: 355 permissions |= 1 356 if flags & property_flags['WRITE_PERMISSION_BIT_1']: 357 permissions |= 2 358 if flags & property_flags['WRITE_PERMISSION_SC'] and permissions == 2: 359 permissions = 4 360 return permissions 361 362def encryption_key_size_from_flags(flags): 363 encryption_key_size = (flags & 0xf000) >> 12 364 if encryption_key_size > 0: 365 encryption_key_size += 1 366 return encryption_key_size 367 368def is_string(text): 369 for item in text.split(" "): 370 if not all(c in string.hexdigits for c in item): 371 return True 372 return False 373 374def add_client_characteristic_configuration(properties): 375 return properties & (property_flags['NOTIFY'] | property_flags['INDICATE']) 376 377def serviceDefinitionComplete(fout): 378 global services 379 if current_service_uuid_string: 380 # fout.write("\n") 381 # update num instances for this service 382 count = 1 383 if current_service_uuid_string in service_counter: 384 count = service_counter[current_service_uuid_string] + 1 385 service_counter[current_service_uuid_string] = count 386 # add old defines without service counter for first instance for backward compatibility 387 if count == 1: 388 defines_for_services.append('#define ATT_SERVICE_%s_START_HANDLE 0x%04x' % (current_service_uuid_string, current_service_start_handle)) 389 defines_for_services.append('#define ATT_SERVICE_%s_END_HANDLE 0x%04x' % (current_service_uuid_string, handle-1)) 390 391 # unified defines indicating instance 392 defines_for_services.append('#define ATT_SERVICE_%s_%02x_START_HANDLE 0x%04x' % (current_service_uuid_string, count, current_service_start_handle)) 393 defines_for_services.append('#define ATT_SERVICE_%s_%02x_END_HANDLE 0x%04x' % (current_service_uuid_string, count, handle-1)) 394 services[current_service_uuid_string+"_" + str(count)] = [current_service_start_handle, handle - 1, count] 395 396def dump_flags(fout, flags): 397 global security_permsission 398 encryption_key_size = encryption_key_size_from_flags(flags) 399 read_permissions = security_permsission[read_permissions_from_flags(flags)] 400 write_permissions = security_permsission[write_permissions_from_flags(flags)] 401 write_indent(fout) 402 fout.write('// ') 403 first = 1 404 if flags & property_flags['READ']: 405 fout.write('READ_%s' % read_permissions) 406 first = 0 407 if flags & (property_flags['WRITE'] | property_flags['WRITE_WITHOUT_RESPONSE']): 408 if not first: 409 fout.write(', ') 410 first = 0 411 fout.write('WRITE_%s' % write_permissions) 412 if encryption_key_size > 0: 413 if not first: 414 fout.write(', ') 415 first = 0 416 fout.write('ENCRYPTION_KEY_SIZE=%u' % encryption_key_size) 417 fout.write('\n') 418 419def database_hash_append_uint8(value): 420 global database_hash_message 421 database_hash_message.append(value) 422 423def database_hash_append_uint16(value): 424 global database_hash_message 425 database_hash_append_uint8(value & 0xff) 426 database_hash_append_uint8((value >> 8) & 0xff) 427 428def database_hash_append_value(value): 429 global database_hash_message 430 for byte in value: 431 database_hash_append_uint8(byte) 432 433def parseService(fout, parts, service_type): 434 global handle 435 global total_size 436 global current_service_uuid_string 437 global current_service_start_handle 438 439 serviceDefinitionComplete(fout) 440 441 read_only_anybody_flags = property_flags['READ']; 442 443 write_indent(fout) 444 fout.write('// 0x%04x %s\n' % (handle, '-'.join(parts))) 445 446 uuid = parseUUID(parts[1]) 447 uuid_size = len(uuid) 448 449 size = 2 + 2 + 2 + uuid_size + 2 450 451 if service_type == 0x2802: 452 size += 4 453 454 write_indent(fout) 455 write_16(fout, size) 456 write_16(fout, read_only_anybody_flags) 457 write_16(fout, handle) 458 write_16(fout, service_type) 459 write_uuid(fout, uuid) 460 fout.write("\n") 461 462 database_hash_append_uint16(handle) 463 database_hash_append_uint16(service_type) 464 database_hash_append_value(uuid) 465 466 current_service_uuid_string = c_string_for_uuid(parts[1]) 467 current_service_start_handle = handle 468 handle = handle + 1 469 total_size = total_size + size 470 471def parsePrimaryService(fout, parts): 472 parseService(fout, parts, 0x2800) 473 474def parseSecondaryService(fout, parts): 475 parseService(fout, parts, 0x2801) 476 477def parseIncludeService(fout, parts): 478 global handle 479 global total_size 480 481 read_only_anybody_flags = property_flags['READ']; 482 483 uuid = parseUUID(parts[1]) 484 uuid_size = len(uuid) 485 if uuid_size > 2: 486 uuid_size = 0 487 488 size = 2 + 2 + 2 + 2 + 4 + uuid_size 489 490 keyUUID = c_string_for_uuid(parts[1]) 491 keys_to_delete = [] 492 493 for (serviceUUID, service) in services.items(): 494 if serviceUUID.startswith(keyUUID): 495 write_indent(fout) 496 fout.write('// 0x%04x %s - range [0x%04x, 0x%04x]\n' % (handle, '-'.join(parts), services[serviceUUID][0], services[serviceUUID][1])) 497 498 write_indent(fout) 499 write_16(fout, size) 500 write_16(fout, read_only_anybody_flags) 501 write_16(fout, handle) 502 write_16(fout, 0x2802) 503 write_16(fout, services[serviceUUID][0]) 504 write_16(fout, services[serviceUUID][1]) 505 if uuid_size > 0: 506 write_uuid(fout, uuid) 507 fout.write("\n") 508 509 database_hash_append_uint16(handle) 510 database_hash_append_uint16(0x2802) 511 database_hash_append_uint16(services[serviceUUID][0]) 512 database_hash_append_uint16(services[serviceUUID][1]) 513 if uuid_size > 0: 514 database_hash_append_value(uuid) 515 516 keys_to_delete.append(serviceUUID) 517 518 handle = handle + 1 519 total_size = total_size + size 520 521 for key in keys_to_delete: 522 services.pop(key) 523 524 525def parseCharacteristic(fout, parts): 526 global handle 527 global total_size 528 global current_characteristic_uuid_string 529 global characteristic_indices 530 531 read_only_anybody_flags = property_flags['READ']; 532 533 # enumerate characteristics with same UUID, using optional name tag if available 534 current_characteristic_uuid_string = c_string_for_uuid(parts[1]); 535 index = 1 536 if current_characteristic_uuid_string in characteristic_indices: 537 index = characteristic_indices[current_characteristic_uuid_string] + 1 538 characteristic_indices[current_characteristic_uuid_string] = index 539 if len(parts) > 4: 540 current_characteristic_uuid_string += '_' + parts[4].upper().replace(' ','_') 541 else: 542 current_characteristic_uuid_string += ('_%02x' % index) 543 544 uuid = parseUUID(parts[1]) 545 uuid_size = len(uuid) 546 properties = parseProperties(parts[2]) 547 value = ', '.join([str(x) for x in parts[3:]]) 548 549 # reliable writes is defined in an extended properties 550 if (properties & property_flags['RELIABLE_WRITE']): 551 properties = properties | property_flags['EXTENDED_PROPERTIES'] 552 553 write_indent(fout) 554 fout.write('// 0x%04x %s - %s\n' % (handle, '-'.join(parts[0:2]), prettyPrintProperties(parts[2]))) 555 556 557 characteristic_properties = gatt_characteristic_properties(properties) 558 size = 2 + 2 + 2 + 2 + (1+2+uuid_size) 559 write_indent(fout) 560 write_16(fout, size) 561 write_16(fout, read_only_anybody_flags) 562 write_16(fout, handle) 563 write_16(fout, 0x2803) 564 write_8(fout, characteristic_properties) 565 write_16(fout, handle+1) 566 write_uuid(fout, uuid) 567 fout.write("\n") 568 total_size = total_size + size 569 570 database_hash_append_uint16(handle) 571 database_hash_append_uint16(0x2803) 572 database_hash_append_uint8(characteristic_properties) 573 database_hash_append_uint16(handle+1) 574 database_hash_append_value(uuid) 575 576 handle = handle + 1 577 578 uuid_is_database_hash = len(uuid) == 2 and uuid[0] == 0x2a and uuid[1] == 0x2b 579 580 size = 2 + 2 + 2 + uuid_size 581 if uuid_is_database_hash: 582 size += 16 583 else: 584 if is_string(value): 585 size = size + len(value) 586 else: 587 size = size + len(value.split()) 588 589 value_flags = att_flags(properties) 590 591 # add UUID128 flag for value handle 592 if uuid_size == 16: 593 value_flags = value_flags | property_flags['LONG_UUID']; 594 595 write_indent(fout) 596 properties_string = prettyPrintProperties(parts[2]) 597 if "DYNAMIC" in properties_string: 598 fout.write('// 0x%04x VALUE %s - %s\n' % (handle, '-'.join(parts[0:2]), prettyPrintProperties(parts[2]))) 599 else: 600 fout.write('// 0x%04x VALUE %s - %s -'"'%s'"'\n' % ( 601 handle, '-'.join(parts[0:2]), prettyPrintProperties(parts[2]), value)) 602 603 dump_flags(fout, value_flags) 604 605 write_indent(fout) 606 write_16(fout, size) 607 write_16(fout, value_flags) 608 write_16(fout, handle) 609 write_uuid(fout, uuid) 610 if uuid_is_database_hash: 611 write_database_hash(fout) 612 else: 613 if is_string(value): 614 write_string(fout, value) 615 else: 616 write_sequence(fout,value) 617 618 fout.write("\n") 619 defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_VALUE_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) 620 handle = handle + 1 621 622 if add_client_characteristic_configuration(properties): 623 # use write permissions and encryption key size from attribute value and set READ_ANYBODY | READ | WRITE | DYNAMIC 624 flags = write_permissions_and_key_size_flags_from_properties(properties) 625 flags |= property_flags['READ'] 626 flags |= property_flags['WRITE'] 627 flags |= property_flags['WRITE_WITHOUT_RESPONSE'] 628 flags |= property_flags['DYNAMIC'] 629 size = 2 + 2 + 2 + 2 + 2 630 631 write_indent(fout) 632 fout.write('// 0x%04x CLIENT_CHARACTERISTIC_CONFIGURATION\n' % (handle)) 633 634 dump_flags(fout, flags) 635 636 write_indent(fout) 637 write_16(fout, size) 638 write_16(fout, flags) 639 write_16(fout, handle) 640 write_16(fout, 0x2902) 641 write_16(fout, 0) 642 fout.write("\n") 643 644 database_hash_append_uint16(handle) 645 database_hash_append_uint16(0x2902) 646 647 defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_CLIENT_CONFIGURATION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) 648 handle = handle + 1 649 650 651 if properties & property_flags['RELIABLE_WRITE']: 652 size = 2 + 2 + 2 + 2 + 2 653 write_indent(fout) 654 fout.write('// 0x%04x CHARACTERISTIC_EXTENDED_PROPERTIES\n' % (handle)) 655 write_indent(fout) 656 write_16(fout, size) 657 write_16(fout, read_only_anybody_flags) 658 write_16(fout, handle) 659 write_16(fout, 0x2900) 660 write_16(fout, 1) # Reliable Write 661 fout.write("\n") 662 663 database_hash_append_uint16(handle) 664 database_hash_append_uint16(0x2900) 665 database_hash_append_uint16(1) 666 667 handle = handle + 1 668 669def parseGenericDynamicDescriptor(fout, parts, uuid, name): 670 global handle 671 global total_size 672 global current_characteristic_uuid_string 673 674 properties = parseProperties(parts[1]) 675 size = 2 + 2 + 2 + 2 676 677 # use write permissions and encryption key size from attribute value and set READ, WRITE, DYNAMIC, READ_ANYBODY 678 flags = write_permissions_and_key_size_flags_from_properties(properties) 679 flags |= property_flags['READ'] 680 flags |= property_flags['WRITE'] 681 flags |= property_flags['DYNAMIC'] 682 683 write_indent(fout) 684 fout.write('// 0x%04x %s-%s\n' % (handle, name, '-'.join(parts[1:]))) 685 686 dump_flags(fout, flags) 687 688 write_indent(fout) 689 write_16(fout, size) 690 write_16(fout, flags) 691 write_16(fout, handle) 692 write_16(fout, uuid) 693 fout.write("\n") 694 695 database_hash_append_uint16(handle) 696 database_hash_append_uint16(uuid) 697 698 defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_%s_HANDLE 0x%04x' % (current_characteristic_uuid_string, name, handle)) 699 handle = handle + 1 700 701def parseGenericDynamicReadOnlyDescriptor(fout, parts, uuid, name): 702 global handle 703 global total_size 704 global current_characteristic_uuid_string 705 706 properties = parseProperties(parts[1]) 707 size = 2 + 2 + 2 + 2 708 709 # use write permissions and encryption key size from attribute value and set READ, DYNAMIC, READ_ANYBODY 710 flags = write_permissions_and_key_size_flags_from_properties(properties) 711 flags |= property_flags['READ'] 712 flags |= property_flags['DYNAMIC'] 713 714 write_indent(fout) 715 fout.write('// 0x%04x %s-%s\n' % (handle, name, '-'.join(parts[1:]))) 716 717 dump_flags(fout, flags) 718 719 write_indent(fout) 720 write_16(fout, size) 721 write_16(fout, flags) 722 write_16(fout, handle) 723 write_16(fout, uuid) 724 fout.write("\n") 725 726 database_hash_append_uint16(handle) 727 database_hash_append_uint16(uuid) 728 729 defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_%s_HANDLE 0x%04x' % (current_characteristic_uuid_string, name, handle)) 730 handle = handle + 1 731 732def parseServerCharacteristicConfiguration(fout, parts): 733 parseGenericDynamicDescriptor(fout, parts, 0x2903, 'SERVER_CONFIGURATION') 734 735def parseCharacteristicFormat(fout, parts): 736 global handle 737 global total_size 738 739 read_only_anybody_flags = property_flags['READ']; 740 741 identifier = parts[1] 742 presentation_formats[identifier] = handle 743 # print("format '%s' with handle %d\n" % (identifier, handle)) 744 745 format = parts[2] 746 exponent = parts[3] 747 unit = parseUUID(parts[4]) 748 name_space = parts[5] 749 description = parseUUID(parts[6]) 750 751 size = 2 + 2 + 2 + 2 + 7 752 753 write_indent(fout) 754 fout.write('// 0x%04x CHARACTERISTIC_FORMAT-%s\n' % (handle, '-'.join(parts[1:]))) 755 write_indent(fout) 756 write_16(fout, size) 757 write_16(fout, read_only_anybody_flags) 758 write_16(fout, handle) 759 write_16(fout, 0x2904) 760 write_sequence(fout, format) 761 write_sequence(fout, exponent) 762 write_uuid(fout, unit) 763 write_sequence(fout, name_space) 764 write_uuid(fout, description) 765 fout.write("\n") 766 767 database_hash_append_uint16(handle) 768 database_hash_append_uint16(0x2904) 769 770 handle = handle + 1 771 772 773def parseCharacteristicAggregateFormat(fout, parts): 774 global handle 775 global total_size 776 777 read_only_anybody_flags = property_flags['READ']; 778 size = 2 + 2 + 2 + 2 + (len(parts)-1) * 2 779 780 write_indent(fout) 781 fout.write('// 0x%04x CHARACTERISTIC_AGGREGATE_FORMAT-%s\n' % (handle, '-'.join(parts[1:]))) 782 write_indent(fout) 783 write_16(fout, size) 784 write_16(fout, read_only_anybody_flags) 785 write_16(fout, handle) 786 write_16(fout, 0x2905) 787 for identifier in parts[1:]: 788 if not identifier in presentation_formats: 789 print(parts) 790 print("ERROR: identifier '%s' in CHARACTERISTIC_AGGREGATE_FORMAT undefined" % identifier) 791 sys.exit(1) 792 format_handle = presentation_formats[identifier] 793 write_16(fout, format_handle) 794 fout.write("\n") 795 796 database_hash_append_uint16(handle) 797 database_hash_append_uint16(0x2905) 798 799 handle = handle + 1 800 801def parseExternalReportReference(fout, parts): 802 global handle 803 global total_size 804 805 read_only_anybody_flags = property_flags['READ']; 806 size = 2 + 2 + 2 + 2 + 2 807 808 report_uuid = int(parts[2], 16) 809 810 write_indent(fout) 811 fout.write('// 0x%04x EXTERNAL_REPORT_REFERENCE-%s\n' % (handle, '-'.join(parts[1:]))) 812 write_indent(fout) 813 write_16(fout, size) 814 write_16(fout, read_only_anybody_flags) 815 write_16(fout, handle) 816 write_16(fout, 0x2907) 817 write_16(fout, report_uuid) 818 fout.write("\n") 819 handle = handle + 1 820 821def parseReportReference(fout, parts): 822 global handle 823 global total_size 824 825 read_only_anybody_flags = property_flags['READ']; 826 size = 2 + 2 + 2 + 2 + 1 + 1 827 828 report_id = parts[2] 829 report_type = parts[3] 830 831 write_indent(fout) 832 fout.write('// 0x%04x REPORT_REFERENCE-%s\n' % (handle, '-'.join(parts[1:]))) 833 write_indent(fout) 834 write_16(fout, size) 835 write_16(fout, read_only_anybody_flags) 836 write_16(fout, handle) 837 write_16(fout, 0x2908) 838 write_sequence(fout, report_id) 839 write_sequence(fout, report_type) 840 fout.write("\n") 841 handle = handle + 1 842 843def parseNumberOfDigitals(fout, parts): 844 global handle 845 global total_size 846 847 read_only_anybody_flags = property_flags['READ']; 848 size = 2 + 2 + 2 + 2 + 1 849 850 no_of_digitals = parts[1] 851 852 write_indent(fout) 853 fout.write('// 0x%04x NUMBER_OF_DIGITALS-%s\n' % (handle, '-'.join(parts[1:]))) 854 write_indent(fout) 855 write_16(fout, size) 856 write_16(fout, read_only_anybody_flags) 857 write_16(fout, handle) 858 write_16(fout, 0x2909) 859 write_sequence(fout, no_of_digitals) 860 fout.write("\n") 861 handle = handle + 1 862 863def parseLines(fname_in, fin, fout): 864 global handle 865 global total_size 866 867 line_count = 0; 868 for line in fin: 869 line = line.strip("\n\r ") 870 line_count += 1 871 872 if line.startswith("//"): 873 fout.write(" //" + line.lstrip('/') + '\n') 874 continue 875 876 if line.startswith("#import"): 877 imported_file = '' 878 parts = re.match('#import\s+<(.*)>\w*',line) 879 if parts and len(parts.groups()) == 1: 880 imported_file = parts.groups()[0] 881 parts = re.match('#import\s+"(.*)"\w*',line) 882 if parts and len(parts.groups()) == 1: 883 imported_file = parts.groups()[0] 884 if len(imported_file) == 0: 885 print('ERROR: #import in file %s - line %u neither <name.gatt> nor "name.gatt" form', (fname_in, line_count)) 886 continue 887 888 imported_file = getFile( imported_file ) 889 print("Importing %s" % imported_file) 890 try: 891 imported_fin = codecs.open (imported_file, encoding='utf-8') 892 fout.write('\n\n // ' + line + ' -- BEGIN\n') 893 parseLines(imported_file, imported_fin, fout) 894 fout.write(' // ' + line + ' -- END\n') 895 except IOError as e: 896 print('ERROR: Import failed. Please check path.') 897 898 continue 899 900 if line.startswith("#TODO"): 901 print ("WARNING: #TODO in file %s - line %u not handled, skipping declaration:" % (fname_in, line_count)) 902 print ("'%s'" % line) 903 fout.write("// " + line + '\n') 904 continue 905 906 if len(line) == 0: 907 continue 908 909 f = io.StringIO(line) 910 parts_list = csv.reader(f, delimiter=',', quotechar='"') 911 912 for parts in parts_list: 913 for index, object in enumerate(parts): 914 parts[index] = object.strip().lstrip('"').rstrip('"') 915 916 if parts[0] == 'PRIMARY_SERVICE': 917 parsePrimaryService(fout, parts) 918 continue 919 920 if parts[0] == 'SECONDARY_SERVICE': 921 parseSecondaryService(fout, parts) 922 continue 923 924 if parts[0] == 'INCLUDE_SERVICE': 925 parseIncludeService(fout, parts) 926 continue 927 928 # 2803 929 if parts[0] == 'CHARACTERISTIC': 930 parseCharacteristic(fout, parts) 931 continue 932 933 # 2900 Characteristic Extended Properties 934 935 # 2901 936 if parts[0] == 'CHARACTERISTIC_USER_DESCRIPTION': 937 parseGenericDynamicDescriptor(fout, parts, 0x2901, 'USER_DESCRIPTION') 938 continue 939 940 941 # 2902 Client Characteristic Configuration - automatically included in Characteristic if 942 # notification / indication is supported 943 if parts[0] == 'CLIENT_CHARACTERISTIC_CONFIGURATION': 944 continue 945 946 # 2903 947 if parts[0] == 'SERVER_CHARACTERISTIC_CONFIGURATION': 948 parseGenericDynamicDescriptor(fout, parts, 0x2903, 'SERVER_CONFIGURATION') 949 continue 950 951 # 2904 952 if parts[0] == 'CHARACTERISTIC_FORMAT': 953 parseCharacteristicFormat(fout, parts) 954 continue 955 956 # 2905 957 if parts[0] == 'CHARACTERISTIC_AGGREGATE_FORMAT': 958 parseCharacteristicAggregateFormat(fout, parts) 959 continue 960 961 # 2906 962 if parts[0] == 'VALID_RANGE': 963 parseGenericDynamicReadOnlyDescriptor(fout, parts, 0x2906, 'VALID_RANGE') 964 continue 965 966 # 2907 967 if parts[0] == 'EXTERNAL_REPORT_REFERENCE': 968 parseExternalReportReference(fout, parts) 969 continue 970 971 # 2908 972 if parts[0] == 'REPORT_REFERENCE': 973 parseReportReference(fout, parts) 974 continue 975 976 # 2909 977 if parts[0] == 'NUMBER_OF_DIGITALS': 978 parseNumberOfDigitals(fout, parts) 979 continue 980 981 # 290A 982 if parts[0] == 'VALUE_TRIGGER_SETTING': 983 parseGenericDynamicDescriptor(fout, parts, 0x290A, 'VALUE_TRIGGER_SETTING') 984 continue 985 986 # 290B 987 if parts[0] == 'ENVIRONMENTAL_SENSING_CONFIGURATION': 988 parseGenericDynamicDescriptor(fout, parts, 0x290B, 'ENVIRONMENTAL_SENSING_CONFIGURATION') 989 continue 990 991 # 290C 992 if parts[0] == 'ENVIRONMENTAL_SENSING_MEASUREMENT': 993 parseGenericDynamicReadOnlyDescriptor(fout, parts, 0x290C, 'ENVIRONMENTAL_SENSING_MEASUREMENT') 994 continue 995 996 # 290D 997 if parts[0] == 'ENVIRONMENTAL_SENSING_TRIGGER_SETTING': 998 parseGenericDynamicDescriptor(fout, parts, 0x290D, 'ENVIRONMENTAL_SENSING_TRIGGER_SETTING') 999 continue 1000 1001 print("WARNING: unknown token: %s\n" % (parts[0])) 1002 1003def parse(fname_in, fin, fname_out, tool_path, fout): 1004 global handle 1005 global total_size 1006 1007 fout.write(header.format(fname_out, fname_in, tool_path)) 1008 fout.write('{\n') 1009 write_indent(fout) 1010 fout.write('// ATT DB Version\n') 1011 write_indent(fout) 1012 fout.write('1,\n') 1013 fout.write("\n") 1014 1015 parseLines(fname_in, fin, fout) 1016 1017 serviceDefinitionComplete(fout) 1018 write_indent(fout) 1019 fout.write("// END\n"); 1020 write_indent(fout) 1021 write_16(fout,0) 1022 fout.write("\n") 1023 total_size = total_size + 2 1024 1025 fout.write("}; // total size %u bytes \n" % total_size); 1026 1027def listHandles(fout): 1028 fout.write('\n\n') 1029 fout.write('//\n') 1030 fout.write('// list service handle ranges\n') 1031 fout.write('//\n') 1032 for define in defines_for_services: 1033 fout.write(define) 1034 fout.write('\n') 1035 fout.write('\n') 1036 fout.write('//\n') 1037 fout.write('// list mapping between characteristics and handles\n') 1038 fout.write('//\n') 1039 for define in defines_for_characteristics: 1040 fout.write(define) 1041 fout.write('\n') 1042 1043def getFile( fileName ): 1044 for d in include_paths: 1045 fullFile = os.path.normpath(d + os.sep + fileName) # because Windows exists 1046 # print("test %s" % fullFile) 1047 if os.path.isfile( fullFile ) == True: 1048 return fullFile 1049 print ("'{0}' not found".format( fileName )) 1050 print ("Include paths: %s" % ", ".join(include_paths)) 1051 exit(-1) 1052 1053 1054btstack_root = os.path.abspath(os.path.dirname(sys.argv[0]) + '/..') 1055default_includes = [os.path.normpath(path) for path in [ 1056 btstack_root + '/src/', 1057 btstack_root + '/src/ble/gatt-service/', 1058 btstack_root + '/src/le-audio/gatt-service/', 1059 btstack_root + '/src/mesh/gatt-service/' 1060]] 1061 1062parser = argparse.ArgumentParser(description='BLE GATT configuration generator for use with BTstack') 1063 1064parser.add_argument('-I', action='append', nargs=1, metavar='includes', 1065 help='include search path for .gatt service files and bluetooth_gatt.h (default: %s)' % ", ".join(default_includes)) 1066parser.add_argument('gattfile', metavar='gattfile', type=str, 1067 help='gatt file to be compiled') 1068parser.add_argument('hfile', metavar='hfile', type=str, 1069 help='header file to be generated') 1070 1071args = parser.parse_args() 1072 1073# add include path arguments 1074if args.I != None: 1075 for d in args.I: 1076 include_paths.append(os.path.normpath(d[0])) 1077 1078# append default include paths 1079include_paths.extend(default_includes) 1080 1081try: 1082 # read defines from bluetooth_gatt.h 1083 gen_path = getFile( 'bluetooth_gatt.h' ) 1084 bluetooth_gatt = read_defines(gen_path) 1085 1086 filename = args.hfile 1087 fin = codecs.open (args.gattfile, encoding='utf-8') 1088 1089 # pass 1: create temp .h file 1090 ftemp = tempfile.TemporaryFile(mode='w+t') 1091 parse(args.gattfile, fin, filename, sys.argv[0], ftemp) 1092 listHandles(ftemp) 1093 1094 # calc GATT Database Hash 1095 db_hash = aes_cmac(bytearray(16), database_hash_message) 1096 if isinstance(db_hash, str): 1097 # python2 1098 db_hash_sequence = [('0x%02x' % ord(i)) for i in db_hash] 1099 elif isinstance(db_hash, bytes): 1100 # python3 1101 db_hash_sequence = [('0x%02x' % i) for i in db_hash] 1102 else: 1103 print("AES CMAC returns unexpected type %s, abort" % type(db_hash)) 1104 sys.exit(1) 1105 # reverse hash to get little endian 1106 db_hash_sequence.reverse() 1107 db_hash_string = ', '.join(db_hash_sequence) + ', ' 1108 1109 # pass 2: insert GATT Database Hash 1110 fout = open (filename, 'w') 1111 ftemp.seek(0) 1112 for line in ftemp: 1113 fout.write(line.replace('THE-DATABASE-HASH', db_hash_string)) 1114 fout.close() 1115 ftemp.close() 1116 1117 print('Created %s' % filename) 1118 1119except IOError as e: 1120 parser.print_help() 1121 print(e) 1122 sys.exit(1) 1123 1124print('Compilation successful!\n') 1125