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