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