1#!/usr/bin/env python 2# 3# BLE GATT configuration generator for use with BTstack, v0.1 4# Copyright 2011 Matthias Ringwald 5# 6# Format of input file: 7# PRIMARY_SERVICE, SERVICE_UUID 8# CHARACTERISTIC, ATTRIBUTE_TYPE_UUID, [READ | WRITE | DYNAMIC], VALUE 9 10import codecs 11import csv 12import io 13import os 14import re 15import string 16 17header = ''' 18// {0} generated from {1} for BTstack 19 20// binary representation 21// attribute size in bytes (16), flags(16), handle (16), uuid (16/128), value(...) 22 23#include <stdint.h> 24 25const uint8_t profile_data[] = 26''' 27 28usage = ''' 29Usage: ./compile_gatt.py profile.gatt profile.h 30''' 31 32import re 33import sys 34 35print(''' 36BLE configuration generator for use with BTstack, v0.1 37Copyright 2011 Matthias Ringwald 38''') 39 40assigned_uuids = { 41 'GAP_SERVICE' : 0x1800, 42 'GATT_SERVICE' : 0x1801, 43 'GAP_DEVICE_NAME' : 0x2a00, 44 'GAP_APPEARANCE' : 0x2a01, 45 'GAP_PERIPHERAL_PRIVACY_FLAG' : 0x2A02, 46 'GAP_RECONNECTION_ADDRESS' : 0x2A03, 47 'GAP_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS' : 0x2A04, 48 'GATT_SERVICE_CHANGED' : 0x2a05, 49} 50 51property_flags = { 52 'BROADCAST' : 0x01, 53 'READ' : 0x02, 54 'WRITE_WITHOUT_RESPONSE' : 0x04, 55 'WRITE' : 0x08, 56 'NOTIFY': 0x10, 57 'INDICATE' : 0x20, 58 'AUTHENTICATED_SIGNED_WRITE' : 0x40, 59 'EXTENDED_PROPERTIES' : 0x80, 60 # custom BTstack extension 61 'DYNAMIC': 0x100, 62 'LONG_UUID': 0x200, 63 'AUTHENTICATION_REQUIRED': 0x400, 64 'AUTHORIZATION_REQUIRED': 0x800, 65 'ENCRYPTION_KEY_SIZE_7': 0x6000, 66 'ENCRYPTION_KEY_SIZE_8': 0x7000, 67 'ENCRYPTION_KEY_SIZE_9': 0x8000, 68 'ENCRYPTION_KEY_SIZE_10': 0x9000, 69 'ENCRYPTION_KEY_SIZE_11': 0xa000, 70 'ENCRYPTION_KEY_SIZE_12': 0xb000, 71 'ENCRYPTION_KEY_SIZE_13': 0xc000, 72 'ENCRYPTION_KEY_SIZE_14': 0xd000, 73 'ENCRYPTION_KEY_SIZE_15': 0xe000, 74 'ENCRYPTION_KEY_SIZE_16': 0xf000, 75 # only used by gatt compiler >= 0xffff 76 # Extended Properties 77 'RELIABLE_WRITE': 0x10000, 78} 79 80services = dict() 81characteristic_indices = dict() 82presentation_formats = dict() 83current_service_uuid_string = "" 84current_service_start_handle = 0 85current_characteristic_uuid_string = "" 86defines = [] 87 88handle = 1 89total_size = 0 90 91def read_defines(infile): 92 defines = dict() 93 with open (infile, 'rt') as fin: 94 for line in fin: 95 parts = re.match('#define\s+(\w+)\s+(\w+)',line) 96 if parts and len(parts.groups()) == 2: 97 (key, value) = parts.groups() 98 defines[key] = int(value, 16) 99 return defines 100 101def keyForUUID(uuid): 102 keyUUID = "" 103 for i in uuid: 104 keyUUID += "%02x" % i 105 return keyUUID 106 107def c_string_for_uuid(uuid): 108 return uuid.replace('-', '_') 109 110def twoByteLEFor(value): 111 return [ (value & 0xff), (value >> 8)] 112 113def is_128bit_uuid(text): 114 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): 115 return True 116 return False 117 118def parseUUID128(uuid): 119 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) 120 uuid_bytes = [] 121 for i in range(8, 0, -1): 122 uuid_bytes = uuid_bytes + twoByteLEFor(int(parts.group(i),16)) 123 return uuid_bytes 124 125def parseUUID(uuid): 126 if uuid in assigned_uuids: 127 return twoByteLEFor(assigned_uuids[uuid]) 128 uuid_upper = uuid.upper().replace('.','_') 129 if uuid_upper in bluetooth_gatt: 130 return twoByteLEFor(bluetooth_gatt[uuid_upper]) 131 if is_128bit_uuid(uuid): 132 return parseUUID128(uuid) 133 uuidInt = int(uuid, 16) 134 return twoByteLEFor(uuidInt) 135 136def parseProperties(properties): 137 value = 0 138 parts = properties.split("|") 139 for property in parts: 140 property = property.strip() 141 if property in property_flags: 142 value |= property_flags[property] 143 else: 144 print("WARNING: property %s undefined" % (property)) 145 return value 146 147def write_8(fout, value): 148 fout.write( "0x%02x, " % (value & 0xff)) 149 150def write_16(fout, value): 151 fout.write('0x%02x, 0x%02x, ' % (value & 0xff, (value >> 8) & 0xff)) 152 153def write_uuid(uuid): 154 for byte in uuid: 155 fout.write( "0x%02x, " % byte) 156 157def write_string(fout, text): 158 for l in text.lstrip('"').rstrip('"'): 159 write_8(fout, ord(l)) 160 161def write_sequence(fout, text): 162 parts = text.split() 163 for part in parts: 164 fout.write("0x%s, " % (part.strip())) 165 166def write_indent(fout): 167 fout.write(" ") 168 169def is_string(text): 170 for item in text.split(" "): 171 if not all(c in string.hexdigits for c in item): 172 return True 173 return False 174 175def add_client_characteristic_configuration(properties): 176 return properties & (property_flags['NOTIFY'] | property_flags['INDICATE']) 177 178def parseService(fout, parts, service_type): 179 global handle 180 global total_size 181 global current_service_uuid_string 182 global current_service_start_handle 183 global services 184 185 if current_service_uuid_string: 186 fout.write("\n") 187 # print("append service %s = [%d, %d]\n" % (current_characteristic_uuid_string, current_service_start_handle, handle-1)) 188 services[current_service_uuid_string] = [current_service_start_handle, handle-1] 189 190 property = property_flags['READ']; 191 192 write_indent(fout) 193 fout.write('// 0x%04x %s\n' % (handle, '-'.join(parts))) 194 195 uuid = parseUUID(parts[1]) 196 uuid_size = len(uuid) 197 198 size = 2 + 2 + 2 + uuid_size + 2 199 200 if service_type == 0x2802: 201 size += 4 202 203 write_indent(fout) 204 write_16(fout, size) 205 write_16(fout, property) 206 write_16(fout, handle) 207 write_16(fout, service_type) 208 write_uuid(uuid) 209 fout.write("\n") 210 211 current_service_uuid_string = keyForUUID(uuid) 212 current_service_start_handle = handle 213 handle = handle + 1 214 total_size = total_size + size 215 216def parsePrimaryService(fout, parts): 217 parseService(fout, parts, 0x2800) 218 219def parseSecondaryService(fout, parts): 220 parseService(fout, parts, 0x2801) 221 222def parseIncludeService(fout, parts): 223 global handle 224 global total_size 225 226 property = property_flags['READ']; 227 228 write_indent(fout) 229 fout.write('// 0x%04x %s\n' % (handle, '-'.join(parts))) 230 231 uuid = parseUUID(parts[1]) 232 uuid_size = len(uuid) 233 if uuid_size > 2: 234 uuid_size = 0 235 # print("Include Service ", keyForUUID(uuid)) 236 237 size = 2 + 2 + 2 + 2 + 4 + uuid_size 238 239 keyUUID = keyForUUID(uuid) 240 241 write_indent(fout) 242 write_16(fout, size) 243 write_16(fout, property) 244 write_16(fout, handle) 245 write_16(fout, 0x2802) 246 write_16(fout, services[keyUUID][0]) 247 write_16(fout, services[keyUUID][1]) 248 if uuid_size > 0: 249 write_uuid(uuid) 250 fout.write("\n") 251 252 handle = handle + 1 253 total_size = total_size + size 254 255 256def parseCharacteristic(fout, parts): 257 global handle 258 global total_size 259 global current_characteristic_uuid_string 260 global characteristic_indices 261 262 property_read = property_flags['READ']; 263 264 # enumerate characteristics with same UUID, using optional name tag if available 265 current_characteristic_uuid_string = c_string_for_uuid(parts[1]); 266 index = 1 267 if current_characteristic_uuid_string in characteristic_indices: 268 index = characteristic_indices[current_characteristic_uuid_string] + 1 269 characteristic_indices[current_characteristic_uuid_string] = index 270 if len(parts) > 4: 271 current_characteristic_uuid_string += '_' + parts[4].upper().replace(' ','_') 272 else: 273 current_characteristic_uuid_string += ('_%02x' % index) 274 275 uuid = parseUUID(parts[1]) 276 uuid_size = len(uuid) 277 properties = parseProperties(parts[2]) 278 value = ', '.join([str(x) for x in parts[3:]]) 279 280 # reliable writes is defined in an extended properties 281 if (properties & property_flags['RELIABLE_WRITE']): 282 properties = properties | property_flags['EXTENDED_PROPERTIES'] 283 284 write_indent(fout) 285 fout.write('// 0x%04x %s\n' % (handle, '-'.join(parts[0:3]))) 286 287 size = 2 + 2 + 2 + 2 + (1+2+uuid_size) 288 write_indent(fout) 289 write_16(fout, size) 290 write_16(fout, property_read) 291 write_16(fout, handle) 292 write_16(fout, 0x2803) 293 write_8(fout, properties) 294 write_16(fout, handle+1) 295 write_uuid(uuid) 296 fout.write("\n") 297 handle = handle + 1 298 total_size = total_size + size 299 300 size = 2 + 2 + 2 + uuid_size 301 if is_string(value): 302 size = size + len(value) 303 else: 304 size = size + len(value.split()) 305 306 if uuid_size == 16: 307 properties = properties | property_flags['LONG_UUID']; 308 309 write_indent(fout) 310 fout.write('// 0x%04x VALUE-%s-'"'%s'"'\n' % (handle, '-'.join(parts[1:3]),value)) 311 write_indent(fout) 312 write_16(fout, size) 313 write_16(fout, properties) 314 write_16(fout, handle) 315 write_uuid(uuid) 316 if is_string(value): 317 write_string(fout, value) 318 else: 319 write_sequence(fout,value) 320 321 fout.write("\n") 322 defines.append('#define ATT_CHARACTERISTIC_%s_VALUE_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) 323 handle = handle + 1 324 325 if add_client_characteristic_configuration(properties): 326 size = 2 + 2 + 2 + 2 + 2 327 write_indent(fout) 328 fout.write('// 0x%04x CLIENT_CHARACTERISTIC_CONFIGURATION\n' % (handle)) 329 write_indent(fout) 330 write_16(fout, size) 331 write_16(fout, property_flags['READ'] | property_flags['WRITE'] | property_flags['DYNAMIC']) 332 write_16(fout, handle) 333 write_16(fout, 0x2902) 334 write_16(fout, 0) 335 fout.write("\n") 336 defines.append('#define ATT_CHARACTERISTIC_%s_CLIENT_CONFIGURATION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) 337 handle = handle + 1 338 339 if properties & property_flags['RELIABLE_WRITE']: 340 size = 2 + 2 + 2 + 2 + 2 341 write_indent(fout) 342 fout.write('// 0x%04x CHARACTERISTIC_EXTENDED_PROPERTIES\n' % (handle)) 343 write_indent(fout) 344 write_16(fout, size) 345 write_16(fout, property_flags['READ']) 346 write_16(fout, handle) 347 write_16(fout, 0x2900) 348 write_16(fout, 1) # Reliable Write 349 fout.write("\n") 350 handle = handle + 1 351 352def parseCharacteristicUserDescription(fout, parts): 353 global handle 354 global total_size 355 global current_characteristic_uuid_string 356 357 properties = parseProperties(parts[1]) 358 value = parts[2] 359 360 size = 2 + 2 + 2 + 2 361 if is_string(value): 362 size = size + len(value) - 2 363 else: 364 size = size + len(value.split()) 365 366 write_indent(fout) 367 fout.write('// 0x%04x CHARACTERISTIC_USER_DESCRIPTION-%s\n' % (handle, '-'.join(parts[1:]))) 368 write_indent(fout) 369 write_16(fout, size) 370 write_16(fout, properties) 371 write_16(fout, handle) 372 write_16(fout, 0x2901) 373 if is_string(value): 374 write_string(fout, value) 375 else: 376 write_sequence(fout,value) 377 fout.write("\n") 378 defines.append('#define ATT_CHARACTERISTIC_%s_USER_DESCRIPTION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) 379 handle = handle + 1 380 381def parseServerCharacteristicConfiguration(fout, parts): 382 global handle 383 global total_size 384 global current_characteristic_uuid_string 385 386 properties = parseProperties(parts[1]) 387 properties = properties | property_flags['DYNAMIC'] 388 size = 2 + 2 + 2 + 2 389 390 write_indent(fout) 391 fout.write('// 0x%04x SERVER_CHARACTERISTIC_CONFIGURATION-%s\n' % (handle, '-'.join(parts[1:]))) 392 write_indent(fout) 393 write_16(fout, size) 394 write_16(fout, properties) 395 write_16(fout, handle) 396 write_16(fout, 0x2903) 397 fout.write("\n") 398 defines.append('#define ATT_CHARACTERISTIC_%s_SERVER_CONFIGURATION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) 399 handle = handle + 1 400 401def parseCharacteristicFormat(fout, parts): 402 global handle 403 global total_size 404 405 property_read = property_flags['READ']; 406 407 identifier = parts[1] 408 presentation_formats[identifier] = handle 409 # print("format '%s' with handle %d\n" % (identifier, handle)) 410 411 format = parts[2] 412 exponent = parts[3] 413 unit = parseUUID(parts[4]) 414 name_space = parts[5] 415 description = parseUUID(parts[6]) 416 417 size = 2 + 2 + 2 + 2 + 7 418 419 write_indent(fout) 420 fout.write('// 0x%04x CHARACTERISTIC_FORMAT-%s\n' % (handle, '-'.join(parts[1:]))) 421 write_indent(fout) 422 write_16(fout, size) 423 write_16(fout, property_read) 424 write_16(fout, handle) 425 write_16(fout, 0x2904) 426 write_sequence(fout, format) 427 write_sequence(fout, exponent) 428 write_uuid(unit) 429 write_sequence(fout, name_space) 430 write_uuid(description) 431 fout.write("\n") 432 handle = handle + 1 433 434 435def parseCharacteristicAggregateFormat(fout, parts): 436 global handle 437 global total_size 438 439 property_read = property_flags['READ']; 440 size = 2 + 2 + 2 + 2 + (len(parts)-1) * 2 441 442 write_indent(fout) 443 fout.write('// 0x%04x CHARACTERISTIC_AGGREGATE_FORMAT-%s\n' % (handle, '-'.join(parts[1:]))) 444 write_indent(fout) 445 write_16(fout, size) 446 write_16(fout, property_read) 447 write_16(fout, handle) 448 write_16(fout, 0x2905) 449 for identifier in parts[1:]: 450 format_handle = presentation_formats[identifier] 451 if format == 0: 452 print("ERROR: identifier '%s' in CHARACTERISTIC_AGGREGATE_FORMAT undefined" % identifier) 453 sys.exit(1) 454 write_16(fout, format_handle) 455 fout.write("\n") 456 handle = handle + 1 457 458def parseReportReference(fout, parts): 459 global handle 460 global total_size 461 462 property_read = property_flags['READ']; 463 size = 2 + 2 + 2 + 2 + 1 + 1 464 465 report_id = parts[1] 466 report_type = parts[2] 467 468 write_indent(fout) 469 fout.write('// 0x%04x REPORT_REFERENCE-%s\n' % (handle, '-'.join(parts[1:]))) 470 write_indent(fout) 471 write_16(fout, size) 472 write_16(fout, property_read) 473 write_16(fout, handle) 474 write_16(fout, 0x2908) 475 write_sequence(fout, report_id) 476 write_sequence(fout, report_type) 477 fout.write("\n") 478 handle = handle + 1 479 480 481def parseNumberOfDigitals(fout, parts): 482 global handle 483 global total_size 484 485 property_read = property_flags['READ']; 486 size = 2 + 2 + 2 + 2 + 1 487 488 no_of_digitals = parts[1] 489 490 write_indent(fout) 491 fout.write('// 0x%04x NUMBER_OF_DIGITALS-%s\n' % (handle, '-'.join(parts[1:]))) 492 write_indent(fout) 493 write_16(fout, size) 494 write_16(fout, property_read) 495 write_16(fout, handle) 496 write_16(fout, 0x2909) 497 write_sequence(fout, no_of_digitals) 498 fout.write("\n") 499 handle = handle + 1 500 501 502def parse(fname_in, fin, fname_out, fout): 503 global handle 504 global total_size 505 506 fout.write(header.format(fname_out, fname_in)) 507 fout.write('{\n') 508 509 line_count = 0; 510 for line in fin: 511 line = line.strip("\n\r ") 512 line_count += 1 513 514 if line.startswith("//"): 515 fout.write("//" + line.lstrip('/') + '\n') 516 continue 517 518 if line.startswith("#"): 519 print ("WARNING: #TODO in line %u not handled, skipping declaration:" % line_count) 520 print ("'%s'" % line) 521 fout.write("// " + line + '\n') 522 continue 523 524 if len(line) == 0: 525 continue 526 527 f = io.StringIO(line) 528 parts_list = csv.reader(f, delimiter=',', quotechar='"') 529 530 for parts in parts_list: 531 for index, object in enumerate(parts): 532 parts[index] = object.strip().lstrip('"').rstrip('"') 533 534 if parts[0] == 'PRIMARY_SERVICE': 535 parsePrimaryService(fout, parts) 536 continue 537 538 if parts[0] == 'SECONDARY_SERVICE': 539 parseSecondaryService(fout, parts) 540 continue 541 542 if parts[0] == 'INCLUDE_SERVICE': 543 parseIncludeService(fout, parts) 544 continue 545 546 # 2803 547 if parts[0] == 'CHARACTERISTIC': 548 parseCharacteristic(fout, parts) 549 continue 550 551 # 2900 Characteristic Extended Properties 552 553 # 2901 554 if parts[0] == 'CHARACTERISTIC_USER_DESCRIPTION': 555 parseCharacteristicUserDescription(fout, parts) 556 continue 557 558 559 # 2902 Client Characteristic Configuration - automatically included in Characteristic if 560 # notification / indication is supported 561 if parts[0] == 'CHARACTERISTIC_USER_DESCRIPTION': 562 continue 563 564 # 2903 565 if parts[0] == 'SERVER_CHARACTERISTIC_CONFIGURATION': 566 parseServerCharacteristicConfiguration(fout, parts) 567 continue 568 569 # 2904 570 if parts[0] == 'CHARACTERISTIC_FORMAT': 571 parseCharacteristicFormat(fout, parts) 572 continue 573 574 # 2905 575 if parts[0] == 'CHARACTERISTIC_AGGREGATE_FORMAT': 576 parseCharacteristicAggregateFormat(fout, parts) 577 continue 578 579 # 2906 580 if parts[0] == 'VALID_RANGE': 581 print("WARNING: %s not implemented yet\n" % (parts[0])) 582 continue 583 584 # 2907 585 if parts[0] == 'EXTERNAL_REPORT_REFERENCE': 586 print("WARNING: %s not implemented yet\n" % (parts[0])) 587 continue 588 589 # 2908 590 if parts[0] == 'REPORT_REFERENCE': 591 parseReportReference(fout, parts) 592 continue 593 594 # 2909 595 if parts[0] == 'NUMBER_OF_DIGITALS': 596 parseNumberOfDigitals(fout, parts) 597 continue 598 599 # 290A 600 if parts[0] == 'VALUE_TRIGGER_SETTING': 601 print("WARNING: %s not implemented yet\n" % (parts[0])) 602 continue 603 604 # 290B 605 if parts[0] == 'ENVIRONMENTAL_SENSING_CONFIGURATION': 606 print("WARNING: %s not implemented yet\n" % (parts[0])) 607 continue 608 609 # 290C 610 if parts[0] == 'ENVIRONMENTAL_SENSING_MEASUREMENT': 611 print("WARNING: %s not implemented yet\n" % (parts[0])) 612 continue 613 614 # 290D 615 if parts[0] == 'ENVIRONMENTAL_SENSING_TRIGGER_SETTING': 616 print("WARNING: %s not implemented yet\n" % (parts[0])) 617 continue 618 619 # 2906 620 if parts[0] == 'VALID_RANGE': 621 print("WARNING: %s not implemented yet\n" % (parts[0])) 622 continue 623 624 print("WARNING: unknown token: %s\n" % (parts[0])) 625 626 write_indent(fout) 627 fout.write("// END\n"); 628 write_indent(fout) 629 write_16(fout,0) 630 fout.write("\n") 631 total_size = total_size + 2 632 633 fout.write("}; // total size %u bytes \n" % total_size); 634 635def listHandles(fout): 636 fout.write('\n\n') 637 fout.write('//\n') 638 fout.write('// list mapping between characteristics and handles\n') 639 fout.write('//\n') 640 for define in defines: 641 fout.write(define) 642 fout.write('\n') 643 644if (len(sys.argv) < 3): 645 print(usage) 646 sys.exit(1) 647try: 648 # read defines from bluetooth_gatt.h 649 btstack_root = os.path.abspath(os.path.dirname(sys.argv[0]) + '/..') 650 gen_path = btstack_root + '/src/bluetooth_gatt.h' 651 bluetooth_gatt = read_defines(gen_path) 652 653 filename = sys.argv[2] 654 fin = codecs.open (sys.argv[1], encoding='utf-8') 655 fout = open (filename, 'w') 656 parse(sys.argv[1], fin, filename, fout) 657 listHandles(fout) 658 fout.close() 659 print('Created %s' % filename) 660 661except IOError as e: 662 print(usage) 663 sys.exit(1) 664 665print('Compilation successful!\n') 666