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