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