compile_gatt.py 35 KB

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