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