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