xref: /btstack/tool/compile_gatt.py (revision b165f97b80eef122c410af6eea7328b135983d75)
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