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