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