mirror of
https://github.com/bluekitchen/btstack.git
synced 2025-01-15 19:53:23 +00:00
d48e511ecb
Add clang-format off to header. Useful when the auto-generated headers are checked-in a repo with clang-format enabled.
1126 lines
37 KiB
Python
Executable File
1126 lines
37 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# BLE GATT configuration generator for use with BTstack
|
|
# Copyright 2019 BlueKitchen GmbH
|
|
#
|
|
# Format of input file:
|
|
# PRIMARY_SERVICE, SERVICE_UUID
|
|
# CHARACTERISTIC, ATTRIBUTE_TYPE_UUID, [READ | WRITE | DYNAMIC], VALUE
|
|
|
|
# dependencies:
|
|
# - pip3 install pycryptodomex
|
|
# alternatively, the pycryptodome package can be used instead
|
|
# - pip3 install pycryptodome
|
|
|
|
import codecs
|
|
import csv
|
|
import io
|
|
import os
|
|
import re
|
|
import string
|
|
import sys
|
|
import argparse
|
|
import tempfile
|
|
|
|
have_crypto = True
|
|
# try to import PyCryptodome independent from PyCrypto
|
|
try:
|
|
from Cryptodome.Cipher import AES
|
|
from Cryptodome.Hash import CMAC
|
|
except ImportError:
|
|
# fallback: try to import PyCryptodome as (an almost drop-in) replacement for the PyCrypto library
|
|
try:
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Hash import CMAC
|
|
except ImportError:
|
|
have_crypto = False
|
|
print("\n[!] PyCryptodome required to calculate GATT Database Hash but not installed (using random value instead)")
|
|
print("[!] Please install PyCryptodome, e.g. 'pip3 install pycryptodomex' or 'pip3 install pycryptodome'\n")
|
|
|
|
header = '''
|
|
// clang-format off
|
|
// {0} generated from {1} for BTstack
|
|
// it needs to be regenerated when the .gatt file is updated.
|
|
|
|
// To generate {0}:
|
|
// {2} {1} {0}
|
|
|
|
// att db format version 1
|
|
|
|
// binary attribute representation:
|
|
// - size in bytes (16), flags(16), handle (16), uuid (16/128), value(...)
|
|
|
|
#include <stdint.h>
|
|
|
|
// Reference: https://en.cppreference.com/w/cpp/feature_test
|
|
#if __cplusplus >= 200704L
|
|
constexpr
|
|
#endif
|
|
const uint8_t profile_data[] =
|
|
'''
|
|
|
|
print('''
|
|
BLE configuration generator for use with BTstack
|
|
Copyright 2018 BlueKitchen GmbH
|
|
''')
|
|
|
|
assigned_uuids = {
|
|
'GAP_SERVICE' : 0x1800,
|
|
'GATT_SERVICE' : 0x1801,
|
|
'GAP_DEVICE_NAME' : 0x2a00,
|
|
'GAP_APPEARANCE' : 0x2a01,
|
|
'GAP_PERIPHERAL_PRIVACY_FLAG' : 0x2A02,
|
|
'GAP_RECONNECTION_ADDRESS' : 0x2A03,
|
|
'GAP_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS' : 0x2A04,
|
|
'GAP_CENTRAL_ADDRESS_RESOLUTION' : 0x2aa6,
|
|
'GAP_RESOLVABLE_PRIVATE_ADDRESS_ONLY' : 0x2AC9,
|
|
'GAP_ENCRYPTED_DATA_KEY_MATERIAL' : 0x2B88,
|
|
'GAP_LE_GATT_SECURITY_LEVELS' : 0x2BF5,
|
|
'GATT_SERVICE_CHANGED' : 0x2a05,
|
|
'GATT_CLIENT_SUPPORTED_FEATURES' : 0x2b29,
|
|
'GATT_SERVER_SUPPORTED_FEATURES' : 0x2b3a,
|
|
'GATT_DATABASE_HASH' : 0x2b2a
|
|
}
|
|
|
|
security_permsission = ['ANYBODY','ENCRYPTED', 'AUTHENTICATED', 'AUTHORIZED', 'AUTHENTICATED_SC']
|
|
|
|
property_flags = {
|
|
# GATT Characteristic Properties
|
|
'BROADCAST' : 0x01,
|
|
'READ' : 0x02,
|
|
'WRITE_WITHOUT_RESPONSE' : 0x04,
|
|
'WRITE' : 0x08,
|
|
'NOTIFY': 0x10,
|
|
'INDICATE' : 0x20,
|
|
'AUTHENTICATED_SIGNED_WRITE' : 0x40,
|
|
'EXTENDED_PROPERTIES' : 0x80,
|
|
# custom BTstack extension
|
|
'DYNAMIC': 0x100,
|
|
'LONG_UUID': 0x200,
|
|
|
|
# read permissions
|
|
'READ_PERMISSION_BIT_0': 0x400,
|
|
'READ_PERMISSION_BIT_1': 0x800,
|
|
|
|
#
|
|
'ENCRYPTION_KEY_SIZE_7': 0x6000,
|
|
'ENCRYPTION_KEY_SIZE_8': 0x7000,
|
|
'ENCRYPTION_KEY_SIZE_9': 0x8000,
|
|
'ENCRYPTION_KEY_SIZE_10': 0x9000,
|
|
'ENCRYPTION_KEY_SIZE_11': 0xa000,
|
|
'ENCRYPTION_KEY_SIZE_12': 0xb000,
|
|
'ENCRYPTION_KEY_SIZE_13': 0xc000,
|
|
'ENCRYPTION_KEY_SIZE_14': 0xd000,
|
|
'ENCRYPTION_KEY_SIZE_15': 0xe000,
|
|
'ENCRYPTION_KEY_SIZE_16': 0xf000,
|
|
'ENCRYPTION_KEY_SIZE_MASK': 0xf000,
|
|
|
|
# only used by gatt compiler >= 0xffff
|
|
# Extended Properties
|
|
'RELIABLE_WRITE': 0x00010000,
|
|
'AUTHENTICATION_REQUIRED': 0x00020000,
|
|
'AUTHORIZATION_REQUIRED': 0x00040000,
|
|
'READ_ANYBODY': 0x00080000,
|
|
'READ_ENCRYPTED': 0x00100000,
|
|
'READ_AUTHENTICATED': 0x00200000,
|
|
'READ_AUTHENTICATED_SC': 0x00400000,
|
|
'READ_AUTHORIZED': 0x00800000,
|
|
'WRITE_ANYBODY': 0x01000000,
|
|
'WRITE_ENCRYPTED': 0x02000000,
|
|
'WRITE_AUTHENTICATED': 0x04000000,
|
|
'WRITE_AUTHENTICATED_SC': 0x08000000,
|
|
'WRITE_AUTHORIZED': 0x10000000,
|
|
|
|
# Broadcast, Notify, Indicate, Extended Properties are only used to describe a GATT Characteristic, but are free to use with att_db
|
|
# - write permissions
|
|
'WRITE_PERMISSION_BIT_0': 0x01,
|
|
'WRITE_PERMISSION_BIT_1': 0x10,
|
|
# - SC required
|
|
'READ_PERMISSION_SC': 0x20,
|
|
'WRITE_PERMISSION_SC': 0x80,
|
|
}
|
|
|
|
services = dict()
|
|
characteristic_indices = dict()
|
|
presentation_formats = dict()
|
|
current_service_uuid_string = ""
|
|
current_service_start_handle = 0
|
|
current_characteristic_uuid_string = ""
|
|
defines_for_characteristics = []
|
|
defines_for_services = []
|
|
include_paths = []
|
|
database_hash_message = bytearray()
|
|
service_counter = {}
|
|
|
|
handle = 1
|
|
total_size = 0
|
|
|
|
def aes_cmac(key, n):
|
|
if have_crypto:
|
|
cobj = CMAC.new(key, ciphermod=AES)
|
|
cobj.update(n)
|
|
return cobj.digest()
|
|
else:
|
|
# return random value
|
|
return os.urandom(16)
|
|
|
|
def read_defines(infile):
|
|
defines = dict()
|
|
with open (infile, 'rt') as fin:
|
|
for line in fin:
|
|
parts = re.match('#define\\s+(\\w+)\\s+(\\w+)',line)
|
|
if parts and len(parts.groups()) == 2:
|
|
(key, value) = parts.groups()
|
|
defines[key] = int(value, 16)
|
|
return defines
|
|
|
|
def keyForUUID(uuid):
|
|
keyUUID = ""
|
|
for i in uuid:
|
|
keyUUID += "%02x" % i
|
|
return keyUUID
|
|
|
|
def c_string_for_uuid(uuid):
|
|
return uuid.replace('-', '_')
|
|
|
|
def twoByteLEFor(value):
|
|
return [ (value & 0xff), (value >> 8)]
|
|
|
|
def is_128bit_uuid(text):
|
|
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):
|
|
return True
|
|
return False
|
|
|
|
def parseUUID128(uuid):
|
|
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)
|
|
uuid_bytes = []
|
|
for i in range(8, 0, -1):
|
|
uuid_bytes = uuid_bytes + twoByteLEFor(int(parts.group(i),16))
|
|
return uuid_bytes
|
|
|
|
def parseUUID(uuid):
|
|
if uuid in assigned_uuids:
|
|
return twoByteLEFor(assigned_uuids[uuid])
|
|
uuid_upper = uuid.upper().replace('.','_')
|
|
if uuid_upper in bluetooth_gatt:
|
|
return twoByteLEFor(bluetooth_gatt[uuid_upper])
|
|
if is_128bit_uuid(uuid):
|
|
return parseUUID128(uuid)
|
|
uuidInt = int(uuid, 16)
|
|
return twoByteLEFor(uuidInt)
|
|
|
|
def parseProperties(properties):
|
|
value = 0
|
|
parts = properties.split("|")
|
|
for property in parts:
|
|
property = property.strip()
|
|
if property in property_flags:
|
|
value |= property_flags[property]
|
|
else:
|
|
print("WARNING: property %s undefined" % (property))
|
|
|
|
return value
|
|
|
|
def prettyPrintProperties(properties):
|
|
value = ""
|
|
parts = properties.split("|")
|
|
for property in parts:
|
|
property = property.strip()
|
|
if property in property_flags:
|
|
if value != "":
|
|
value += " | "
|
|
value += property
|
|
else:
|
|
print("WARNING: property %s undefined" % (property))
|
|
|
|
return value
|
|
|
|
|
|
def gatt_characteristic_properties(properties):
|
|
return properties & 0xff
|
|
|
|
def att_flags(properties):
|
|
# drop Broadcast (0x01), Notify (0x10), Indicate (0x20), Extended Properties (0x80) - not used for flags
|
|
properties &= 0xffffff4e
|
|
|
|
# rw permissions distinct
|
|
distinct_permissions_used = properties & (
|
|
property_flags['READ_AUTHORIZED'] |
|
|
property_flags['READ_AUTHENTICATED_SC'] |
|
|
property_flags['READ_AUTHENTICATED'] |
|
|
property_flags['READ_ENCRYPTED'] |
|
|
property_flags['READ_ANYBODY'] |
|
|
property_flags['WRITE_AUTHORIZED'] |
|
|
property_flags['WRITE_AUTHENTICATED'] |
|
|
property_flags['WRITE_AUTHENTICATED_SC'] |
|
|
property_flags['WRITE_ENCRYPTED'] |
|
|
property_flags['WRITE_ANYBODY']
|
|
) != 0
|
|
|
|
# post process properties
|
|
encryption_key_size_specified = (properties & property_flags['ENCRYPTION_KEY_SIZE_MASK']) != 0
|
|
|
|
# if distinct permissions not used and encyrption key size specified -> set READ/WRITE Encrypted
|
|
if encryption_key_size_specified and not distinct_permissions_used:
|
|
properties |= property_flags['READ_ENCRYPTED'] | property_flags['WRITE_ENCRYPTED']
|
|
|
|
# if distinct permissions not used and authentication is requires -> set READ/WRITE Authenticated
|
|
if properties & property_flags['AUTHENTICATION_REQUIRED'] and not distinct_permissions_used:
|
|
properties |= property_flags['READ_AUTHENTICATED'] | property_flags['WRITE_AUTHENTICATED']
|
|
|
|
# if distinct permissions not used and authorized is requires -> set READ/WRITE Authorized
|
|
if properties & property_flags['AUTHORIZATION_REQUIRED'] and not distinct_permissions_used:
|
|
properties |= property_flags['READ_AUTHORIZED'] | property_flags['WRITE_AUTHORIZED']
|
|
|
|
# determine read/write security requirements
|
|
read_security_level = 0
|
|
write_security_level = 0
|
|
read_requires_sc = False
|
|
write_requires_sc = False
|
|
if properties & property_flags['READ_AUTHORIZED']:
|
|
read_security_level = 3
|
|
elif properties & property_flags['READ_AUTHENTICATED']:
|
|
read_security_level = 2
|
|
elif properties & property_flags['READ_AUTHENTICATED_SC']:
|
|
read_security_level = 2
|
|
read_requires_sc = True
|
|
elif properties & property_flags['READ_ENCRYPTED']:
|
|
read_security_level = 1
|
|
if properties & property_flags['WRITE_AUTHORIZED']:
|
|
write_security_level = 3
|
|
elif properties & property_flags['WRITE_AUTHENTICATED']:
|
|
write_security_level = 2
|
|
elif properties & property_flags['WRITE_AUTHENTICATED_SC']:
|
|
write_security_level = 2
|
|
write_requires_sc = True
|
|
elif properties & property_flags['WRITE_ENCRYPTED']:
|
|
write_security_level = 1
|
|
|
|
# map security requirements to flags
|
|
if read_security_level & 2:
|
|
properties |= property_flags['READ_PERMISSION_BIT_1']
|
|
if read_security_level & 1:
|
|
properties |= property_flags['READ_PERMISSION_BIT_0']
|
|
if read_requires_sc:
|
|
properties |= property_flags['READ_PERMISSION_SC']
|
|
if write_security_level & 2:
|
|
properties |= property_flags['WRITE_PERMISSION_BIT_1']
|
|
if write_security_level & 1:
|
|
properties |= property_flags['WRITE_PERMISSION_BIT_0']
|
|
if write_requires_sc:
|
|
properties |= property_flags['WRITE_PERMISSION_SC']
|
|
|
|
return properties
|
|
|
|
def write_permissions_and_key_size_flags_from_properties(properties):
|
|
return att_flags(properties) & (property_flags['ENCRYPTION_KEY_SIZE_MASK'] | property_flags['WRITE_PERMISSION_BIT_0'] | property_flags['WRITE_PERMISSION_BIT_1'])
|
|
|
|
def write_8(fout, value):
|
|
fout.write( "0x%02x, " % (value & 0xff))
|
|
|
|
def write_16(fout, value):
|
|
fout.write('0x%02x, 0x%02x, ' % (value & 0xff, (value >> 8) & 0xff))
|
|
|
|
def write_uuid(fout, uuid):
|
|
for byte in uuid:
|
|
fout.write( "0x%02x, " % byte)
|
|
|
|
def write_string(fout, text):
|
|
for l in text.lstrip('"').rstrip('"'):
|
|
write_8(fout, ord(l))
|
|
|
|
def write_sequence(fout, text):
|
|
parts = text.split()
|
|
for part in parts:
|
|
fout.write("0x%s, " % (part.strip()))
|
|
|
|
def write_database_hash(fout):
|
|
fout.write("THE-DATABASE-HASH")
|
|
|
|
def write_indent(fout):
|
|
fout.write(" ")
|
|
|
|
def read_permissions_from_flags(flags):
|
|
permissions = 0
|
|
if flags & property_flags['READ_PERMISSION_BIT_0']:
|
|
permissions |= 1
|
|
if flags & property_flags['READ_PERMISSION_BIT_1']:
|
|
permissions |= 2
|
|
if flags & property_flags['READ_PERMISSION_SC'] and permissions == 2:
|
|
permissions = 4
|
|
return permissions
|
|
|
|
def write_permissions_from_flags(flags):
|
|
permissions = 0
|
|
if flags & property_flags['WRITE_PERMISSION_BIT_0']:
|
|
permissions |= 1
|
|
if flags & property_flags['WRITE_PERMISSION_BIT_1']:
|
|
permissions |= 2
|
|
if flags & property_flags['WRITE_PERMISSION_SC'] and permissions == 2:
|
|
permissions = 4
|
|
return permissions
|
|
|
|
def encryption_key_size_from_flags(flags):
|
|
encryption_key_size = (flags & 0xf000) >> 12
|
|
if encryption_key_size > 0:
|
|
encryption_key_size += 1
|
|
return encryption_key_size
|
|
|
|
def is_string(text):
|
|
for item in text.split(" "):
|
|
if not all(c in string.hexdigits for c in item):
|
|
return True
|
|
return False
|
|
|
|
def add_client_characteristic_configuration(properties):
|
|
return properties & (property_flags['NOTIFY'] | property_flags['INDICATE'])
|
|
|
|
def serviceDefinitionComplete(fout):
|
|
global services
|
|
if current_service_uuid_string:
|
|
# fout.write("\n")
|
|
# update num instances for this service
|
|
count = 1
|
|
if current_service_uuid_string in service_counter:
|
|
count = service_counter[current_service_uuid_string] + 1
|
|
service_counter[current_service_uuid_string] = count
|
|
# add old defines without service counter for first instance for backward compatibility
|
|
if count == 1:
|
|
defines_for_services.append('#define ATT_SERVICE_%s_START_HANDLE 0x%04x' % (current_service_uuid_string, current_service_start_handle))
|
|
defines_for_services.append('#define ATT_SERVICE_%s_END_HANDLE 0x%04x' % (current_service_uuid_string, handle-1))
|
|
|
|
# unified defines indicating instance
|
|
defines_for_services.append('#define ATT_SERVICE_%s_%02x_START_HANDLE 0x%04x' % (current_service_uuid_string, count, current_service_start_handle))
|
|
defines_for_services.append('#define ATT_SERVICE_%s_%02x_END_HANDLE 0x%04x' % (current_service_uuid_string, count, handle-1))
|
|
services[current_service_uuid_string+"_" + str(count)] = [current_service_start_handle, handle - 1, count]
|
|
|
|
def dump_flags(fout, flags):
|
|
global security_permsission
|
|
encryption_key_size = encryption_key_size_from_flags(flags)
|
|
read_permissions = security_permsission[read_permissions_from_flags(flags)]
|
|
write_permissions = security_permsission[write_permissions_from_flags(flags)]
|
|
write_indent(fout)
|
|
fout.write('// ')
|
|
first = 1
|
|
if flags & property_flags['READ']:
|
|
fout.write('READ_%s' % read_permissions)
|
|
first = 0
|
|
if flags & (property_flags['WRITE'] | property_flags['WRITE_WITHOUT_RESPONSE']):
|
|
if not first:
|
|
fout.write(', ')
|
|
first = 0
|
|
fout.write('WRITE_%s' % write_permissions)
|
|
if encryption_key_size > 0:
|
|
if not first:
|
|
fout.write(', ')
|
|
first = 0
|
|
fout.write('ENCRYPTION_KEY_SIZE=%u' % encryption_key_size)
|
|
fout.write('\n')
|
|
|
|
def database_hash_append_uint8(value):
|
|
global database_hash_message
|
|
database_hash_message.append(value)
|
|
|
|
def database_hash_append_uint16(value):
|
|
global database_hash_message
|
|
database_hash_append_uint8(value & 0xff)
|
|
database_hash_append_uint8((value >> 8) & 0xff)
|
|
|
|
def database_hash_append_value(value):
|
|
global database_hash_message
|
|
for byte in value:
|
|
database_hash_append_uint8(byte)
|
|
|
|
def parseService(fout, parts, service_type):
|
|
global handle
|
|
global total_size
|
|
global current_service_uuid_string
|
|
global current_service_start_handle
|
|
|
|
serviceDefinitionComplete(fout)
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x %s\n' % (handle, '-'.join(parts)))
|
|
|
|
uuid = parseUUID(parts[1])
|
|
uuid_size = len(uuid)
|
|
|
|
size = 2 + 2 + 2 + uuid_size + 2
|
|
|
|
if service_type == 0x2802:
|
|
size += 4
|
|
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, service_type)
|
|
write_uuid(fout, uuid)
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(service_type)
|
|
database_hash_append_value(uuid)
|
|
|
|
current_service_uuid_string = c_string_for_uuid(parts[1])
|
|
current_service_start_handle = handle
|
|
handle = handle + 1
|
|
total_size = total_size + size
|
|
|
|
def parsePrimaryService(fout, parts):
|
|
parseService(fout, parts, 0x2800)
|
|
|
|
def parseSecondaryService(fout, parts):
|
|
parseService(fout, parts, 0x2801)
|
|
|
|
def parseIncludeService(fout, parts):
|
|
global handle
|
|
global total_size
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
|
|
uuid = parseUUID(parts[1])
|
|
uuid_size = len(uuid)
|
|
if uuid_size > 2:
|
|
uuid_size = 0
|
|
|
|
size = 2 + 2 + 2 + 2 + 4 + uuid_size
|
|
|
|
keyUUID = c_string_for_uuid(parts[1])
|
|
keys_to_delete = []
|
|
|
|
for (serviceUUID, service) in services.items():
|
|
if serviceUUID.startswith(keyUUID):
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x %s - range [0x%04x, 0x%04x]\n' % (handle, '-'.join(parts), services[serviceUUID][0], services[serviceUUID][1]))
|
|
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2802)
|
|
write_16(fout, services[serviceUUID][0])
|
|
write_16(fout, services[serviceUUID][1])
|
|
if uuid_size > 0:
|
|
write_uuid(fout, uuid)
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(0x2802)
|
|
database_hash_append_uint16(services[serviceUUID][0])
|
|
database_hash_append_uint16(services[serviceUUID][1])
|
|
if uuid_size > 0:
|
|
database_hash_append_value(uuid)
|
|
|
|
keys_to_delete.append(serviceUUID)
|
|
|
|
handle = handle + 1
|
|
total_size = total_size + size
|
|
|
|
for key in keys_to_delete:
|
|
services.pop(key)
|
|
|
|
|
|
def parseCharacteristic(fout, parts):
|
|
global handle
|
|
global total_size
|
|
global current_characteristic_uuid_string
|
|
global characteristic_indices
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
|
|
# enumerate characteristics with same UUID, using optional name tag if available
|
|
current_characteristic_uuid_string = c_string_for_uuid(parts[1]);
|
|
index = 1
|
|
if current_characteristic_uuid_string in characteristic_indices:
|
|
index = characteristic_indices[current_characteristic_uuid_string] + 1
|
|
characteristic_indices[current_characteristic_uuid_string] = index
|
|
if len(parts) > 4:
|
|
current_characteristic_uuid_string += '_' + parts[4].upper().replace(' ','_')
|
|
else:
|
|
current_characteristic_uuid_string += ('_%02x' % index)
|
|
|
|
uuid = parseUUID(parts[1])
|
|
uuid_size = len(uuid)
|
|
properties = parseProperties(parts[2])
|
|
value = ', '.join([str(x) for x in parts[3:]])
|
|
|
|
# reliable writes is defined in an extended properties
|
|
if (properties & property_flags['RELIABLE_WRITE']):
|
|
properties = properties | property_flags['EXTENDED_PROPERTIES']
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x %s - %s\n' % (handle, '-'.join(parts[0:2]), prettyPrintProperties(parts[2])))
|
|
|
|
|
|
characteristic_properties = gatt_characteristic_properties(properties)
|
|
size = 2 + 2 + 2 + 2 + (1+2+uuid_size)
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2803)
|
|
write_8(fout, characteristic_properties)
|
|
write_16(fout, handle+1)
|
|
write_uuid(fout, uuid)
|
|
fout.write("\n")
|
|
total_size = total_size + size
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(0x2803)
|
|
database_hash_append_uint8(characteristic_properties)
|
|
database_hash_append_uint16(handle+1)
|
|
database_hash_append_value(uuid)
|
|
|
|
handle = handle + 1
|
|
|
|
uuid_is_database_hash = len(uuid) == 2 and uuid[0] == 0x2a and uuid[1] == 0x2b
|
|
|
|
size = 2 + 2 + 2 + uuid_size
|
|
if uuid_is_database_hash:
|
|
size += 16
|
|
else:
|
|
if is_string(value):
|
|
size = size + len(value)
|
|
else:
|
|
size = size + len(value.split())
|
|
|
|
value_flags = att_flags(properties)
|
|
|
|
# add UUID128 flag for value handle
|
|
if uuid_size == 16:
|
|
value_flags = value_flags | property_flags['LONG_UUID'];
|
|
|
|
write_indent(fout)
|
|
properties_string = prettyPrintProperties(parts[2])
|
|
if "DYNAMIC" in properties_string:
|
|
fout.write('// 0x%04x VALUE %s - %s\n' % (handle, '-'.join(parts[0:2]), prettyPrintProperties(parts[2])))
|
|
else:
|
|
fout.write('// 0x%04x VALUE %s - %s -'"'%s'"'\n' % (
|
|
handle, '-'.join(parts[0:2]), prettyPrintProperties(parts[2]), value))
|
|
|
|
dump_flags(fout, value_flags)
|
|
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, value_flags)
|
|
write_16(fout, handle)
|
|
write_uuid(fout, uuid)
|
|
if uuid_is_database_hash:
|
|
write_database_hash(fout)
|
|
else:
|
|
if is_string(value):
|
|
write_string(fout, value)
|
|
else:
|
|
write_sequence(fout,value)
|
|
|
|
fout.write("\n")
|
|
defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_VALUE_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle))
|
|
handle = handle + 1
|
|
|
|
if add_client_characteristic_configuration(properties):
|
|
# use write permissions and encryption key size from attribute value and set READ_ANYBODY | READ | WRITE | DYNAMIC
|
|
flags = write_permissions_and_key_size_flags_from_properties(properties)
|
|
flags |= property_flags['READ']
|
|
flags |= property_flags['WRITE']
|
|
flags |= property_flags['WRITE_WITHOUT_RESPONSE']
|
|
flags |= property_flags['DYNAMIC']
|
|
size = 2 + 2 + 2 + 2 + 2
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x CLIENT_CHARACTERISTIC_CONFIGURATION\n' % (handle))
|
|
|
|
dump_flags(fout, flags)
|
|
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2902)
|
|
write_16(fout, 0)
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(0x2902)
|
|
|
|
defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_CLIENT_CONFIGURATION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle))
|
|
handle = handle + 1
|
|
|
|
|
|
if properties & property_flags['RELIABLE_WRITE']:
|
|
size = 2 + 2 + 2 + 2 + 2
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x CHARACTERISTIC_EXTENDED_PROPERTIES\n' % (handle))
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2900)
|
|
write_16(fout, 1) # Reliable Write
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(0x2900)
|
|
database_hash_append_uint16(1)
|
|
|
|
handle = handle + 1
|
|
|
|
def parseGenericDynamicDescriptor(fout, parts, uuid, name):
|
|
global handle
|
|
global total_size
|
|
global current_characteristic_uuid_string
|
|
|
|
properties = parseProperties(parts[1])
|
|
size = 2 + 2 + 2 + 2
|
|
|
|
# use write permissions and encryption key size from attribute value and set READ, WRITE, DYNAMIC, READ_ANYBODY
|
|
flags = write_permissions_and_key_size_flags_from_properties(properties)
|
|
flags |= property_flags['READ']
|
|
flags |= property_flags['WRITE']
|
|
flags |= property_flags['DYNAMIC']
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x %s-%s\n' % (handle, name, '-'.join(parts[1:])))
|
|
|
|
dump_flags(fout, flags)
|
|
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, uuid)
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(uuid)
|
|
|
|
defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_%s_HANDLE 0x%04x' % (current_characteristic_uuid_string, name, handle))
|
|
handle = handle + 1
|
|
|
|
def parseGenericDynamicReadOnlyDescriptor(fout, parts, uuid, name):
|
|
global handle
|
|
global total_size
|
|
global current_characteristic_uuid_string
|
|
|
|
properties = parseProperties(parts[1])
|
|
size = 2 + 2 + 2 + 2
|
|
|
|
# use write permissions and encryption key size from attribute value and set READ, DYNAMIC, READ_ANYBODY
|
|
flags = write_permissions_and_key_size_flags_from_properties(properties)
|
|
flags |= property_flags['READ']
|
|
flags |= property_flags['DYNAMIC']
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x %s-%s\n' % (handle, name, '-'.join(parts[1:])))
|
|
|
|
dump_flags(fout, flags)
|
|
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, uuid)
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(uuid)
|
|
|
|
defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_%s_HANDLE 0x%04x' % (current_characteristic_uuid_string, name, handle))
|
|
handle = handle + 1
|
|
|
|
def parseServerCharacteristicConfiguration(fout, parts):
|
|
parseGenericDynamicDescriptor(fout, parts, 0x2903, 'SERVER_CONFIGURATION')
|
|
|
|
def parseCharacteristicFormat(fout, parts):
|
|
global handle
|
|
global total_size
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
|
|
identifier = parts[1]
|
|
presentation_formats[identifier] = handle
|
|
# print("format '%s' with handle %d\n" % (identifier, handle))
|
|
|
|
format = parts[2]
|
|
exponent = parts[3]
|
|
unit = parseUUID(parts[4])
|
|
name_space = parts[5]
|
|
description = parseUUID(parts[6])
|
|
|
|
size = 2 + 2 + 2 + 2 + 7
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x CHARACTERISTIC_FORMAT-%s\n' % (handle, '-'.join(parts[1:])))
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2904)
|
|
write_sequence(fout, format)
|
|
write_sequence(fout, exponent)
|
|
write_uuid(fout, unit)
|
|
write_sequence(fout, name_space)
|
|
write_uuid(fout, description)
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(0x2904)
|
|
|
|
handle = handle + 1
|
|
|
|
|
|
def parseCharacteristicAggregateFormat(fout, parts):
|
|
global handle
|
|
global total_size
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
size = 2 + 2 + 2 + 2 + (len(parts)-1) * 2
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x CHARACTERISTIC_AGGREGATE_FORMAT-%s\n' % (handle, '-'.join(parts[1:])))
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2905)
|
|
for identifier in parts[1:]:
|
|
if not identifier in presentation_formats:
|
|
print(parts)
|
|
print("ERROR: identifier '%s' in CHARACTERISTIC_AGGREGATE_FORMAT undefined" % identifier)
|
|
sys.exit(1)
|
|
format_handle = presentation_formats[identifier]
|
|
write_16(fout, format_handle)
|
|
fout.write("\n")
|
|
|
|
database_hash_append_uint16(handle)
|
|
database_hash_append_uint16(0x2905)
|
|
|
|
handle = handle + 1
|
|
|
|
def parseExternalReportReference(fout, parts):
|
|
global handle
|
|
global total_size
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
size = 2 + 2 + 2 + 2 + 2
|
|
|
|
report_uuid = int(parts[2], 16)
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x EXTERNAL_REPORT_REFERENCE-%s\n' % (handle, '-'.join(parts[1:])))
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2907)
|
|
write_16(fout, report_uuid)
|
|
fout.write("\n")
|
|
handle = handle + 1
|
|
|
|
def parseReportReference(fout, parts):
|
|
global handle
|
|
global total_size
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
size = 2 + 2 + 2 + 2 + 1 + 1
|
|
|
|
report_id = parts[2]
|
|
report_type = parts[3]
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x REPORT_REFERENCE-%s\n' % (handle, '-'.join(parts[1:])))
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2908)
|
|
write_sequence(fout, report_id)
|
|
write_sequence(fout, report_type)
|
|
fout.write("\n")
|
|
handle = handle + 1
|
|
|
|
def parseNumberOfDigitals(fout, parts):
|
|
global handle
|
|
global total_size
|
|
|
|
read_only_anybody_flags = property_flags['READ'];
|
|
size = 2 + 2 + 2 + 2 + 1
|
|
|
|
no_of_digitals = parts[1]
|
|
|
|
write_indent(fout)
|
|
fout.write('// 0x%04x NUMBER_OF_DIGITALS-%s\n' % (handle, '-'.join(parts[1:])))
|
|
write_indent(fout)
|
|
write_16(fout, size)
|
|
write_16(fout, read_only_anybody_flags)
|
|
write_16(fout, handle)
|
|
write_16(fout, 0x2909)
|
|
write_sequence(fout, no_of_digitals)
|
|
fout.write("\n")
|
|
handle = handle + 1
|
|
|
|
def parseLines(fname_in, fin, fout):
|
|
global handle
|
|
global total_size
|
|
|
|
line_count = 0;
|
|
for line in fin:
|
|
line = line.strip("\n\r ")
|
|
line_count += 1
|
|
|
|
if line.startswith("//"):
|
|
fout.write(" //" + line.lstrip('/') + '\n')
|
|
continue
|
|
|
|
if line.startswith("#import"):
|
|
imported_file = ''
|
|
parts = re.match('#import\\s+<(.*)>\\w*',line)
|
|
if parts and len(parts.groups()) == 1:
|
|
imported_file = parts.groups()[0]
|
|
parts = re.match('#import\\s+"(.*)"\\w*',line)
|
|
if parts and len(parts.groups()) == 1:
|
|
imported_file = parts.groups()[0]
|
|
if len(imported_file) == 0:
|
|
print('ERROR: #import in file %s - line %u neither <name.gatt> nor "name.gatt" form', (fname_in, line_count))
|
|
continue
|
|
|
|
imported_file = getFile( imported_file )
|
|
print("Importing %s" % imported_file)
|
|
try:
|
|
imported_fin = codecs.open (imported_file, encoding='utf-8')
|
|
fout.write('\n\n // ' + line + ' -- BEGIN\n')
|
|
parseLines(imported_file, imported_fin, fout)
|
|
fout.write(' // ' + line + ' -- END\n')
|
|
except IOError as e:
|
|
print('ERROR: Import failed. Please check path.')
|
|
|
|
continue
|
|
|
|
if line.startswith("#TODO"):
|
|
print ("WARNING: #TODO in file %s - line %u not handled, skipping declaration:" % (fname_in, line_count))
|
|
print ("'%s'" % line)
|
|
fout.write("// " + line + '\n')
|
|
continue
|
|
|
|
if len(line) == 0:
|
|
continue
|
|
|
|
f = io.StringIO(line)
|
|
parts_list = csv.reader(f, delimiter=',', quotechar='"')
|
|
|
|
for parts in parts_list:
|
|
for index, object in enumerate(parts):
|
|
parts[index] = object.strip().lstrip('"').rstrip('"')
|
|
|
|
if parts[0] == 'PRIMARY_SERVICE':
|
|
parsePrimaryService(fout, parts)
|
|
continue
|
|
|
|
if parts[0] == 'SECONDARY_SERVICE':
|
|
parseSecondaryService(fout, parts)
|
|
continue
|
|
|
|
if parts[0] == 'INCLUDE_SERVICE':
|
|
parseIncludeService(fout, parts)
|
|
continue
|
|
|
|
# 2803
|
|
if parts[0] == 'CHARACTERISTIC':
|
|
parseCharacteristic(fout, parts)
|
|
continue
|
|
|
|
# 2900 Characteristic Extended Properties
|
|
|
|
# 2901
|
|
if parts[0] == 'CHARACTERISTIC_USER_DESCRIPTION':
|
|
parseGenericDynamicDescriptor(fout, parts, 0x2901, 'USER_DESCRIPTION')
|
|
continue
|
|
|
|
|
|
# 2902 Client Characteristic Configuration - automatically included in Characteristic if
|
|
# notification / indication is supported
|
|
if parts[0] == 'CLIENT_CHARACTERISTIC_CONFIGURATION':
|
|
continue
|
|
|
|
# 2903
|
|
if parts[0] == 'SERVER_CHARACTERISTIC_CONFIGURATION':
|
|
parseGenericDynamicDescriptor(fout, parts, 0x2903, 'SERVER_CONFIGURATION')
|
|
continue
|
|
|
|
# 2904
|
|
if parts[0] == 'CHARACTERISTIC_FORMAT':
|
|
parseCharacteristicFormat(fout, parts)
|
|
continue
|
|
|
|
# 2905
|
|
if parts[0] == 'CHARACTERISTIC_AGGREGATE_FORMAT':
|
|
parseCharacteristicAggregateFormat(fout, parts)
|
|
continue
|
|
|
|
# 2906
|
|
if parts[0] == 'VALID_RANGE':
|
|
parseGenericDynamicReadOnlyDescriptor(fout, parts, 0x2906, 'VALID_RANGE')
|
|
continue
|
|
|
|
# 2907
|
|
if parts[0] == 'EXTERNAL_REPORT_REFERENCE':
|
|
parseExternalReportReference(fout, parts)
|
|
continue
|
|
|
|
# 2908
|
|
if parts[0] == 'REPORT_REFERENCE':
|
|
parseReportReference(fout, parts)
|
|
continue
|
|
|
|
# 2909
|
|
if parts[0] == 'NUMBER_OF_DIGITALS':
|
|
parseNumberOfDigitals(fout, parts)
|
|
continue
|
|
|
|
# 290A
|
|
if parts[0] == 'VALUE_TRIGGER_SETTING':
|
|
parseGenericDynamicDescriptor(fout, parts, 0x290A, 'VALUE_TRIGGER_SETTING')
|
|
continue
|
|
|
|
# 290B
|
|
if parts[0] == 'ENVIRONMENTAL_SENSING_CONFIGURATION':
|
|
parseGenericDynamicDescriptor(fout, parts, 0x290B, 'ENVIRONMENTAL_SENSING_CONFIGURATION')
|
|
continue
|
|
|
|
# 290C
|
|
if parts[0] == 'ENVIRONMENTAL_SENSING_MEASUREMENT':
|
|
parseGenericDynamicReadOnlyDescriptor(fout, parts, 0x290C, 'ENVIRONMENTAL_SENSING_MEASUREMENT')
|
|
continue
|
|
|
|
# 290D
|
|
if parts[0] == 'ENVIRONMENTAL_SENSING_TRIGGER_SETTING':
|
|
parseGenericDynamicDescriptor(fout, parts, 0x290D, 'ENVIRONMENTAL_SENSING_TRIGGER_SETTING')
|
|
continue
|
|
|
|
print("WARNING: unknown token: %s\n" % (parts[0]))
|
|
|
|
def parse(fname_in, fin, fname_out, tool_path, fout):
|
|
global handle
|
|
global total_size
|
|
|
|
fout.write(header.format(fname_out, fname_in, tool_path))
|
|
fout.write('{\n')
|
|
write_indent(fout)
|
|
fout.write('// ATT DB Version\n')
|
|
write_indent(fout)
|
|
fout.write('1,\n')
|
|
fout.write("\n")
|
|
|
|
parseLines(fname_in, fin, fout)
|
|
|
|
serviceDefinitionComplete(fout)
|
|
write_indent(fout)
|
|
fout.write("// END\n");
|
|
write_indent(fout)
|
|
write_16(fout,0)
|
|
fout.write("\n")
|
|
total_size = total_size + 2
|
|
|
|
fout.write("}; // total size %u bytes \n" % total_size);
|
|
|
|
def listHandles(fout):
|
|
fout.write('\n\n')
|
|
fout.write('//\n')
|
|
fout.write('// list service handle ranges\n')
|
|
fout.write('//\n')
|
|
for define in defines_for_services:
|
|
fout.write(define)
|
|
fout.write('\n')
|
|
fout.write('\n')
|
|
fout.write('//\n')
|
|
fout.write('// list mapping between characteristics and handles\n')
|
|
fout.write('//\n')
|
|
for define in defines_for_characteristics:
|
|
fout.write(define)
|
|
fout.write('\n')
|
|
|
|
def getFile( fileName ):
|
|
for d in include_paths:
|
|
fullFile = os.path.normpath(d + os.sep + fileName) # because Windows exists
|
|
# print("test %s" % fullFile)
|
|
if os.path.isfile( fullFile ) == True:
|
|
return fullFile
|
|
print ("'{0}' not found".format( fileName ))
|
|
print ("Include paths: %s" % ", ".join(include_paths))
|
|
exit(-1)
|
|
|
|
|
|
btstack_root = os.path.abspath(os.path.dirname(sys.argv[0]) + '/..')
|
|
default_includes = [os.path.normpath(path) for path in [
|
|
btstack_root + '/src/',
|
|
btstack_root + '/src/ble/gatt-service/',
|
|
btstack_root + '/src/le-audio/gatt-service/',
|
|
btstack_root + '/src/mesh/gatt-service/'
|
|
]]
|
|
|
|
parser = argparse.ArgumentParser(description='BLE GATT configuration generator for use with BTstack')
|
|
|
|
parser.add_argument('-I', action='append', nargs=1, metavar='includes',
|
|
help='include search path for .gatt service files and bluetooth_gatt.h (default: %s)' % ", ".join(default_includes))
|
|
parser.add_argument('gattfile', metavar='gattfile', type=str,
|
|
help='gatt file to be compiled')
|
|
parser.add_argument('hfile', metavar='hfile', type=str,
|
|
help='header file to be generated')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# add include path arguments
|
|
if args.I != None:
|
|
for d in args.I:
|
|
include_paths.append(os.path.normpath(d[0]))
|
|
|
|
# append default include paths
|
|
include_paths.extend(default_includes)
|
|
|
|
try:
|
|
# read defines from bluetooth_gatt.h
|
|
gen_path = getFile( 'bluetooth_gatt.h' )
|
|
bluetooth_gatt = read_defines(gen_path)
|
|
|
|
filename = args.hfile
|
|
fin = codecs.open (args.gattfile, encoding='utf-8')
|
|
|
|
# pass 1: create temp .h file
|
|
ftemp = tempfile.TemporaryFile(mode='w+t')
|
|
parse(args.gattfile, fin, filename, sys.argv[0], ftemp)
|
|
listHandles(ftemp)
|
|
|
|
# calc GATT Database Hash
|
|
db_hash = aes_cmac(bytearray(16), database_hash_message)
|
|
if isinstance(db_hash, str):
|
|
# python2
|
|
db_hash_sequence = [('0x%02x' % ord(i)) for i in db_hash]
|
|
elif isinstance(db_hash, bytes):
|
|
# python3
|
|
db_hash_sequence = [('0x%02x' % i) for i in db_hash]
|
|
else:
|
|
print("AES CMAC returns unexpected type %s, abort" % type(db_hash))
|
|
sys.exit(1)
|
|
# reverse hash to get little endian
|
|
db_hash_sequence.reverse()
|
|
db_hash_string = ', '.join(db_hash_sequence) + ', '
|
|
|
|
# pass 2: insert GATT Database Hash
|
|
fout = open (filename, 'w')
|
|
ftemp.seek(0)
|
|
for line in ftemp:
|
|
fout.write(line.replace('THE-DATABASE-HASH', db_hash_string))
|
|
fout.close()
|
|
ftemp.close()
|
|
|
|
print('Created %s' % filename)
|
|
|
|
except IOError as e:
|
|
parser.print_help()
|
|
print(e)
|
|
sys.exit(1)
|
|
|
|
print('Compilation successful!\n')
|