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