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