如何通过dbus开发bluez(python版)
1.简介
在linux上,如果你正在考虑用bluez开发蓝牙相关功能,应该很快就会查到官方推荐用dbus开发。假如你对dbus不熟悉的话,估计很容易就会两眼发黑,发现网上基本找不到例子,似乎让人很难弄下去。这里我提供个入门方法应该可以方便很多人开发bluez。(另外bluez似乎能编译出一个hci动态库,但由于官方推荐使用dbus,这里不做考虑)
2.开发环境
建议先从python版的代码出发,用python操作dbus会比较简单,bluez的一些例子也是通过python编写。
bluez版本:5.55
3.例子
这里以bluez源码里的 test/test-discovery 为例,了解清楚了bluez如何进行扫描,dbus控制bluez的这一套方法应该也基本能熟悉。
下面是我修改过的test-discovery代码。官方代码似乎是安装python2写的,python3运行会报错。
#!/usr/bin/python3
from __future__ import absolute_import, print_function, unicode_literals
from optparse import OptionParser, make_option
import dbus
import dbus.mainloop.glib
try:
from gi.repository import GObject
except ImportError:
import gobject as GObject
import bluezutils
compact = False
devices = {}
def print_compact(address, properties):
name = ""
address = "<unknown>"
for key, value in properties.iteritems():
if type(value) is dbus.String:
value = unicode(value).encode('ascii', 'replace')
if (key == "Name"):
name = value
elif (key == "Address"):
address = value
if "Logged" in properties:
flag = "*"
else:
flag = " "
print("%s%s %s" % (flag, address, name))
properties["Logged"] = True
def print_normal(address, properties):
print("[ " + address + " ]")
for key in properties.keys():
value = properties[key]
#if type(value) is dbus.String:
#value = unicode(value).encode('ascii', 'replace')
if (key == "Class"):
print(" %s = 0x%06x" % (key, value))
else:
print(" %s = %s" % (key, value))
print()
properties["Logged"] = True
def skip_dev(old_dev, new_dev):
if not "Logged" in old_dev:
return False
if "Name" in old_dev:
return True
if not "Name" in new_dev:
return True
return False
def interfaces_added(path, interfaces):
properties = interfaces["org.bluez.Device1"]
if not properties:
return
if path in devices:
dev = devices[path]
if compact and skip_dev(dev, properties):
return
devices[path] = dict(devices[path].items() + properties.items())
else:
devices[path] = properties
if "Address" in devices[path]:
address = properties["Address"]
else:
address = "<unknown>"
if compact:
print_compact(address, devices[path])
else:
print_normal(address, devices[path])
def properties_changed(interface, changed, invalidated, path):
if interface != "org.bluez.Device1":
return
if path in devices:
dev = devices[path]
if compact and skip_dev(dev, changed):
return
devices[path] = dict(list(devices[path].items()) + list(changed.items()))
else:
devices[path] = changed
if "Address" in devices[path]:
address = devices[path]["Address"]
else:
address = "<unknown>"
if compact:
print_compact(address, devices[path])
else:
print_normal(address, devices[path])
if __name__ == '__main__':
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
option_list = [
make_option("-i", "--device", action="store",
type="string", dest="dev_id"),
make_option("-u", "--uuids", action="store",
type="string", dest="uuids",
help="Filtered service UUIDs [uuid1,uuid2,...]"),
make_option("-r", "--rssi", action="store",
type="int", dest="rssi",
help="RSSI threshold value"),
make_option("-p", "--pathloss", action="store",
type="int", dest="pathloss",
help="Pathloss threshold value"),
make_option("-t", "--transport", action="store",
type="string", dest="transport",
help="Type of scan to run (le/bredr/auto)"),
make_option("-c", "--compact",
action="store_true", dest="compact"),
]
parser = OptionParser(option_list=option_list)
(options, args) = parser.parse_args()
adapter = bluezutils.find_adapter(options.dev_id)
if options.compact:
compact = True;
bus.add_signal_receiver(interfaces_added,
dbus_interface = "org.freedesktop.DBus.ObjectManager",
signal_name = "InterfacesAdded")
bus.add_signal_receiver(properties_changed,
dbus_interface = "org.freedesktop.DBus.Properties",
signal_name = "PropertiesChanged",
arg0 = "org.bluez.Device1",
path_keyword = "path")
om = dbus.Interface(bus.get_object("org.bluez", "/"),
"org.freedesktop.DBus.ObjectManager")
objects = om.GetManagedObjects()
for path, interfaces in objects.items():
if "org.bluez.Device1" in interfaces:
devices[path] = interfaces["org.bluez.Device1"]
scan_filter = dict()
if options.uuids:
uuids = []
uuid_list = options.uuids.split(',')
for uuid in uuid_list:
uuids.append(uuid)
scan_filter.update({ "UUIDs": uuids })
if options.rssi:
scan_filter.update({ "RSSI": dbus.Int16(options.rssi) })
if options.pathloss:
scan_filter.update({ "Pathloss": dbus.UInt16(options.pathloss) })
if options.transport:
scan_filter.update({ "Transport": options.transport })
adapter.SetDiscoveryFilter(scan_filter)
adapter.StartDiscovery()
mainloop = GObject.MainLoop()
mainloop.run()
执行命令(我的环境是yocto构建好的,相关依赖都自动添加上,实际运行时需要考虑依赖):
python3 test-discovery
会不断输出下面这种类型的log:
[ 35:C9:3B:53:D0:99 ]
Address = 35:C9:3B:53:D0:99
AddressType = random
Alias = 35-C9-3B-53-D0-99
Paired = 0
Trusted = 0
Blocked = 0
LegacyPairing = 0
RSSI = -89
Connected = 0
UUIDs = dbus.Array([], signature=dbus.Signature('s'), variant_level=1)
Adapter = /org/bluez/hci0
ManufacturerData = dbus.Dictionary({dbus.UInt16(76): dbus.Array([dbus.Byte(2), dbus.Byte(21), dbus.Byte(38), dbus.Byte(134), dbus.Byte(243), dbus.Byte(156), dbus.Byte(186), dbus.Byte(218), dbus.Byte(70), dbus.Byte(88), dbus.Byte(133), dbus.Byte(74), dbus.Byte(166), dbus.Byte(46), dbus.Byte(126), dbus.Byte(94), dbus.Byte(139), dbus.Byte(141), dbus.Byte(0), dbus.Byte(1), dbus.Byte(0), dbus.Byte(0), dbus.Byte(11)], signature=dbus.Signature('y'), variant_level=1)}, signature=dbus.Signature('qv'), variant_level=1)
ServicesResolved = 0
4.分析
从main函数开始看,我将一些关键的处理抽出:
if __name__ == '__main__':
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
···
adapter = bluezutils.find_adapter(options.dev_id)
···
bus.add_signal_receiver(interfaces_added,
dbus_interface = "org.freedesktop.DBus.ObjectManager",
signal_name = "InterfacesAdded")
bus.add_signal_receiver(properties_changed,
dbus_interface = "org.freedesktop.DBus.Properties",
signal_name = "PropertiesChanged",
arg0 = "org.bluez.Device1",
path_keyword = "path")
···
adapter.StartDiscovery()
mainloop = GObject.MainLoop()
mainloop.run()
简单解释下流程
1.初始化主循环,和dbus
2.获取蓝牙adapter的控制接口
3.注册回调函数,
interfaces_added:对应的扫描的新的蓝牙mac
properties_changed:对应当已扫描到的蓝牙的属性发生变化时触发回调。
4.开始扫描,并进入主循环
从这个角度看dbus的操作就瞬间清晰了,接下来再详细看是如何获取adapter的控制接口
bluezutils.py
import dbus
SERVICE_NAME = "org.bluez"
ADAPTER_INTERFACE = SERVICE_NAME + ".Adapter1"
DEVICE_INTERFACE = SERVICE_NAME + ".Device1"
def get_managed_objects():
bus = dbus.SystemBus()
manager = dbus.Interface(bus.get_object("org.bluez", "/"),
"org.freedesktop.DBus.ObjectManager")
return manager.GetManagedObjects()
def find_adapter(pattern=None):
return find_adapter_in_objects(get_managed_objects(), pattern)
def find_adapter_in_objects(objects, pattern=None):
bus = dbus.SystemBus()
for path, ifaces in objects.items():
adapter = ifaces.get(ADAPTER_INTERFACE)
if adapter is None:
continue
if not pattern or pattern == adapter["Address"] or \
path.endswith(pattern):
obj = bus.get_object(SERVICE_NAME, path)
return dbus.Interface(obj, ADAPTER_INTERFACE)
raise Exception("Bluetooth adapter not found")
这里先只考虑find_adapter_in_objects最后的输出,整理一下后大概是以下的样子
dbus.Interface(bus.get_object("org.bluez", "/org/blue/hci0"), "org.bluez.Adapter1")
现在开始引入dbus的概念:
1.“org.bluez”:对应dbus中的服务名;
2.“/org/blue/hci0”:对应dbus里的对象(但用的时候有时的称呼是路径)
3.“org.bluez.Adapter1”:对应dbus里的接口
4.再将后面的adapter.StartDiscovery()拿来,这个对应的是adapter接口里的方法StartDiscovery
为了更加方便理解这些概念,引入工具d-feet,可以在ubuntu下直接运行
红框:服务
黄框:对象
蓝框:接口
绿框:方法
通过这副图应该能很清晰的了解到dbus各层的概念了。
现在再看一个具体的方法
GetManagedObjects()->(Dict of {Object Path, Dict of{String, Dict of {String, Variant}}} objects)
解析:GetManagedObjects没有入参,出参的结构为字典嵌套字典再嵌套字典,其意义可以参考dbus官方说明(https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager):
The return value of this method is a dict whose keys are object paths. All returned object paths are children of the object path implementing this interface, i.e. their object paths start with the ObjectManager's object path plus '/'.
简单点讲就是获取服务下的所有对象,以及对象里的东西。(备注:像org.freedesktop.DBus.ObjectManager这种类型的接口是dbus是定义的,而不是bluez定义,bluez定义的接口会长这种样子"org.bluez.Adapter1",所以dbus的接口要找dbus的文档,bluez接口的文档在源码里的doc)
再看下find_adapter_in_objects具体的处理:
1.find_adapter_in_objects的入参objects通过GetManagedObjects获取到了全部对象。
2.for path, ifaces in objects.items(),通过迭代获取到每个对象,及其对饮接口
3.adapter = ifaces.get(ADAPTER_INTERFACE),通过尝试打开adapter来确认是否有adapter,一旦获取到了adapter接口,该函数的功能就完成了
最后是信号,使用方法是:
bus.add_signal_receiver(interfaces_added,
dbus_interface = "org.freedesktop.DBus.ObjectManager",
signal_name = "InterfacesAdded")
从前面d-feet的图可以看到,回调里输出的参数应该为
(Object Path, Dict of{String, Dict of {String, Variant}})
再看下interfaces_added函数:
def interfaces_added(path, interfaces):
properties = interfaces["org.bluez.Device1"]
if not properties:
return
if path in devices:
dev = devices[path]
if compact and skip_dev(dev, properties):
return
devices[path] = dict(devices[path].items() + properties.items())
else:
devices[path] = properties
if "Address" in devices[path]:
address = properties["Address"]
else:
address = "<unknown>"
if compact:
print_compact(address, devices[path])
else:
print_normal(address, devices[path])
path对应string
interfaces对应Dict of{String, Dict of {String, Variant}}
到这为止,应该没必要再去找官方文档了吧,单看变量名称应该都能知道这些参数代表什么意思了。
这里再解释下InterfacesAdded信号的作用,当该信号触发时,意味着扫描到了新的蓝牙,对应的在dbus里有新的对象产生(InterfacesAdded的实现是bluez官方写的,dbus官方只定义了接口名称,我也觉得挺奇怪的,加的是对象,名字却叫接口添加)。对应d-feet里
在后一个信号properties_changed
bus.add_signal_receiver(properties_changed,
dbus_interface = "org.freedesktop.DBus.Properties",
signal_name = "PropertiesChanged",
arg0 = "org.bluez.Device1",
path_keyword = "path")
这个对应具体设备里的属性,python这里写的比较简略,我也不详细展开,这里给出d-feet里具体的位置应该就足够理解了,如果后面有时间写c语言版再详细说明
通过以上分析,python要怎么操作bluez应该已经是比较清晰的了,后续一些操作可以参考bluez源码里的其他例子。如果对c语言也比较了解的话,源码里的client也是个很好的例子。