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