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