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