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