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