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