最近在使用bluez的小工具gatttool
,同时也会配合其他小工具一起使用,比如hcitool
,hciconfig
等,但是我发现gatttool
的输出结果很不友好,很难一眼就从众多的handle里面找到自己想要读写的handle,于是就萌生了想要美化下gatttool
的输出的想法:
输出结果对比
之前很长一段时间都是在手机上查看设备的服务,手机上的界面一直是这样的,非常美观:
可以看到,手机的输出非常Nice,一个设备上有哪些服务,这些服务的Characteristic有哪些,他们的属性是什么样子的,一目了然,我们对比一下,gatttool
的输出是这样的:
attr handle = 0x0001, end grp handle = 0x0005 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle = 0x0006, end grp handle = 0x0009 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle = 0x000a, end grp handle = 0x0011 uuid: 0000ffe0-0000-1000-8000-00805f9b34fb
attr handle = 0x0012, end grp handle = 0x0015 uuid: 66666666-6666-6666-6666-666666666666
attr handle = 0x0016, end grp handle = 0x0019 uuid: 86868686-8686-8686-8686-868686868686
是这样的:
handle = 0x0001, uuid = 00002800-0000-1000-8000-00805f9b34fb
handle = 0x0002, uuid = 00002803-0000-1000-8000-00805f9b34fb
handle = 0x0003, uuid = 00002a00-0000-1000-8000-00805f9b34fb
handle = 0x0004, uuid = 00002803-0000-1000-8000-00805f9b34fb
handle = 0x0005, uuid = 00002a01-0000-1000-8000-00805f9b34fb
handle = 0x0006, uuid = 00002800-0000-1000-8000-00805f9b34fb
handle = 0x0007, uuid = 00002803-0000-1000-8000-00805f9b34fb
handle = 0x0008, uuid = 00002a05-0000-1000-8000-00805f9b34fb
handle = 0x0009, uuid = 00002902-0000-1000-8000-00805f9b34fb
handle = 0x000a, uuid = 00002800-0000-1000-8000-00805f9b34fb
handle = 0x000b, uuid = 00002803-0000-1000-8000-00805f9b34fb
handle = 0x000c, uuid = 0000ffe1-0000-1000-8000-00805f9b34fb
handle = 0x000d, uuid = 00002902-0000-1000-8000-00805f9b34fb
handle = 0x000e, uuid = 00002901-0000-1000-8000-00805f9b34fb
handle = 0x000f, uuid = 00002803-0000-1000-8000-00805f9b34fb
handle = 0x0010, uuid = 0000ffe2-0000-1000-8000-00805f9b34fb
handle = 0x0011, uuid = 00002901-0000-1000-8000-00805f9b34fb
handle = 0x0012, uuid = 00002800-0000-1000-8000-00805f9b34fb
handle = 0x0013, uuid = 00002803-0000-1000-8000-00805f9b34fb
handle = 0x0014, uuid = 77777777-7777-7777-7777-777777777777
handle = 0x0015, uuid = 00002902-0000-1000-8000-00805f9b34fb
handle = 0x0016, uuid = 00002800-0000-1000-8000-00805f9b34fb
handle = 0x0017, uuid = 00002803-0000-1000-8000-00805f9b34fb
handle = 0x0018, uuid = 97979797-9797-9797-9797-979797979797
handle = 0x0019, uuid = 00002902-0000-1000-8000-00805f9b34fb
又或者是这样的:
handle = 0x0002, char properties = 0x02, char value handle = 0x0003, uuid = 00002a00-0000-1000-8000-00805f9b34fb
handle = 0x0004, char properties = 0x02, char value handle = 0x0005, uuid = 00002a01-0000-1000-8000-00805f9b34fb
handle = 0x0007, char properties = 0x22, char value handle = 0x0008, uuid = 00002a05-0000-1000-8000-00805f9b34fb
handle = 0x000b, char properties = 0x10, char value handle = 0x000c, uuid = 0000ffe1-0000-1000-8000-00805f9b34fb
handle = 0x000f, char properties = 0x0c, char value handle = 0x0010, uuid = 0000ffe2-0000-1000-8000-00805f9b34fb
handle = 0x0013, char properties = 0x18, char value handle = 0x0014, uuid = 77777777-7777-7777-7777-777777777777
handle = 0x0017, char properties = 0x18, char value handle = 0x0018, uuid = 97979797-9797-9797-9797-979797979797
如果不稍微分析下这个结果的话,很难知道这个设备上包含哪些服务,这些服务包含哪些Characteristic,这些Characteristic的读写属性是咋样的,虽然作为技术宅需要具备这样的能力,但是清晰胜于模糊,简明胜于复杂,于是我想是不是可以写一个脚本把gatttool
的输出美化一下呢?
于是这就是这篇博客的由来,我恰好会一点Python
,于是就想到用Python
来实现这个功能,由于Python
水平不是很高,可能有些地方写得也相对冗余了些,先给大家看下成果吧
图看的出来,我使用的是树莓派_。现在,这样的输出是不是清楚多了,我的设备上有5个服务,其中前2个是系统服务,后面3个是用户定制的服务,每个服务包好几个Characteristic,这些Characteristic的读写属性也标记出来了,但是这里并没有对Properties
作进一步的解析了,其实可以把他们解析成Write Notify
之类的字符串的,以后有时间可能会更新下。
我在图中着重打印出了USER DESC HANDLE
和CLIENT CONFIG HANDLE
,因为这2个属性挺重要的,读一下USER DESC HANDLE
会返回一个字符串,这个字符串是用户对于这个Characteristic的文字描述
$ gatttool -i hci0 -b 12:34:56:C2:9C:C7 --char-read -a 0x0011
Characteristic value/descriptor: 44 61 74 61 20 50 61 74 68 20 52 58 20 44 61 74 61 00
我们看下这段数字代表的字符串是什么:
$ python
>>> a="44 61 74 61 20 50 61 74 68 20 52 58 20 44 61 74 61".split(' ')
>>> "".join([chr(int("0x"+v, 16)) for v in a])
'Data Path RX Data'
就是我手机截图中的字符串
另一个CLIENT CONFIG HANDLE
也很重要,如果设备的某个属性是Notify
的,意思是允许设备给我们发数据,但是默认是不可以发数据的,必须要向这个属性写1才能开启这个数据发送扽功能,写0时关闭,我看到有些同学使用gatttool接收不到设备发来的数据,原因就在这里,具体教程可以看下我的另一篇博客(gatttool命令详解),会有非常详细的介绍
Python实现原理
先说下思路吧,主要是用了Python
的os.popen
管道来调用gatttool
命令,然后读取其输出,最后处理其输出结果的字符串
$ python
>>> import os
>>> output = os.popen("sudo gatttool -i hci0 -b <Device> --primary").read()
我通过这个os.popen
分别调用了--primary, ----characteristics和--char-desc
,把结果汇总起来,最后再输出结果,下面贴出我的源代码,亲测是可以直接运行的
#! /usr/bin/python
import re
import sys
import os
PRIMARY_SERVICE_UUID = "00002800-0000-1000-8000-00805f9b34fb"
SECOND_SERVICE_UUID = "00002801-0000-1000-8000-00805f9b34fb"
INCLUDED_SERVICE_UUID = "00002802-0000-1000-8000-00805f9b34fb"
CHARACTERISTICS_UUID = "00002803-0000-1000-8000-00805f9b34fb"
GATT_USER_DESC_UUID = "00002901-0000-1000-8000-00805f9b34fb"
GATT_CLIENT_CHARAC_UUID = "00002902-0000-1000-8000-00805f9b34fb"
GATT_SERVER_CHARAC_CFG_UUID = "00002903-0000-1000-8000-00805f9b34fb"
class Gatttool:
def __init__(self, address):
self.getPrimaryGattCommand = "sudo gatttool -i hci0 -b %s --primary" % address
self.getCharacteristicsGattCommand = "sudo gatttool -i hci0 -b %s --characteristics" % address
self.getCharDescGattCommand = "sudo gatttool -i hci0 -b %s --char-desc" % address
def getAllPrimarys(self):
primary = os.popen(self.getPrimaryGattCommand).read()
# primary = g_primary
if primary:
return primary.split('\n')
return None
def getAllCharacteristics(self):
# characteristics = g_characteristics
characteristics = os.popen(self.getCharacteristicsGattCommand).read()
if characteristics:
return characteristics.split('\n')
return None
def getAllCharDesc(self):
# descs = g_descs
descs = os.popen(self.getCharDescGattCommand).read()
if descs:
return descs.split('\n')
return None
class Primary:
def __init__(self, startHandle, endHandle, uuid):
self.startHandle = startHandle
self.endHandle = endHandle
self.uuid = uuid
self.characteristics =[]
def addCharacteristics(self, characteristic):
self.characteristics.append(characteristic)
def __str__(self):
return "[Primary] Service UUID: %s Start Handle: %s End Handle: %s" % (self.uuid, self.startHandle, self.endHandle)
class Characteristics:
def __init__(self, charHandle, valueHandle, uuid, properties):
self.charHandle = charHandle
self.valueHandle = valueHandle
self.properties = properties
self.uuid = uuid
self.userDescHandle = None
self.clientConfigHandle = None
def setUserDescHandle(self, handle):
self.userDescHandle = handle
def setClientConfigHandle(self, handle):
self.clientConfigHandle = handle
def __str__(self):
return "[Characteristics] Value Handle: %s Char Handler: %s Properties: %s" % (self.valueHandle, self. charHandle, self.properties)
class CharDesc:
def __init__(self, handle, uuid):
self.handle = handle
self.uuid = uuid
def __str__(self):
return "[CharDesc] UUID: %s Handler: %s" % (self.uuid, self.handle)
class PrettyPrint:
def __init__(self, address):
self.myPrimaryList = ParserUtil().primarysParser(Gatttool(address).getAllPrimarys())
self.myCharacteristics = ParserUtil().characteristicsParser(Gatttool(address).getAllCharacteristics())
self.myDescList = ParserUtil().descParser(Gatttool(address).getAllCharDesc())
for char in self.myCharacteristics:
service = self.findSerivceByHandle(char.valueHandle)
service.addCharacteristics(char)
self.searchUserDescByHandle(char)
self.output()
def getPrettyUUID(self, uuid):
reString = "0000(?P<shortUUID>[0-9a-f]{4})-0000-1000-8000-00805f9b34fb"
match = re.search(reString, uuid)
if match:
return match.groupdict()['shortUUID']
return uuid
def output(self):
print('------------------------------------------------------------------------')
for service in self.myPrimaryList:
print("\033[32mService UUID:\033[0m %s CHAR NUM: %s" % (self.getPrettyUUID(service.uuid).upper(), len(service.characteristics)))
index = 0
for characteristic in service.characteristics:
print(" CHAR%d UUID: %s " % (index, self.getPrettyUUID(characteristic.uuid).upper()))
print(" USER DESC HANDLE: %s CLIENT CONFIG HANDLE: %s" % (characteristic.userDescHandle, characteristic.clientConfigHandle))
print(" VALUE CHAR HANDLE: %s PROPERTIES: %s" % (characteristic.valueHandle, characteristic.properties))
index += 1
print('------------------------------------------------------------------------')
def findSerivceByHandle(self, handle):
for service in self.myPrimaryList:
if int(handle, 16) >= int(service.startHandle, 16) and int(handle, 16) <= int(service.endHandle, 16):
return service
return None
def findDescByHandle(self, handleNum):
for desc in self.myDescList:
if int(desc.handle, 16) == handleNum:
return desc
return None
def nextDescByHandle(self, handleNum):
for desc in self.myDescList:
if int(desc.handle, 16) > handleNum:
return desc, int(desc.handle, 16)
return None, None
def searchUserDescByHandle(self, char):
handleNum = int(char.valueHandle, 16)
desc = self.findDescByHandle(handleNum)
while desc:
if desc.uuid == PRIMARY_SERVICE_UUID or desc.uuid == CHARACTERISTICS_UUID:
return
if desc.uuid == GATT_USER_DESC_UUID:
char.setUserDescHandle(desc.handle)
elif desc.uuid == GATT_CLIENT_CHARAC_UUID:
char.setClientConfigHandle(desc.handle)
desc, handleNum = self.nextDescByHandle(handleNum)
class ParserUtil:
def __init__(self):
pass
def primarysParser(self, primarys):
self.primaryRe = 'attr handle = (?P<startHandle>0x[0-9a-f]{4}), \
end grp handle = (?P<endHandle>0x[0-9a-f]{4}) \
uuid: (?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
ret = []
for primary in primarys:
match = re.search(self.primaryRe, primary)
if match:
obj = match.groupdict()
ret.append(Primary(obj['startHandle'], obj['endHandle'], obj['uuid']))
return ret
def characteristicsParser(self, characteristics):
self.characteristicsRe = 'handle = (?P<charHandle>0x[0-9a-f]{4}), \
char properties = (?P<properties>0x[0-9a-f]{2}), \
char value handle = (?P<valueHandle>0x[0-9a-f]{4}), \
uuid = (?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
ret = []
for characteristic in characteristics:
match = re.search(self.characteristicsRe, characteristic)
if match:
obj = match.groupdict()
ret.append(Characteristics(obj['charHandle'], obj['valueHandle'], obj['uuid'], obj['properties']))
return ret
def descParser(self, descs):
self.descRe = 'handle = (?P<handle>0x[0-9a-f]{4}), \
uuid = (?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
ret = []
for desc in descs:
match = re.search(self.descRe, desc)
if match:
obj = match.groupdict()
ret.append(CharDesc(obj['handle'], obj['uuid']))
return ret
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Invalid Input")
exit(1)
address = sys.argv[1]
PrettyPrint(address)
有需要的同学可以拿过去亲自测试下,欢迎给我提意见~