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