如何通过dbus开发bluez(python版)

本文介绍了如何在Linux上使用dbus开发BlueZ的蓝牙功能,包括设置开发环境、使用python示例(如test-discovery)以及信号处理和对象管理。通过实例分析,帮助读者理解dbus在BlueZ中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如何通过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也是个很好的例子。

### BlueZ BLE 开发教程和文档 #### 蓝牙协议栈简介 BlueZLinux 下实现蓝牙协议栈的一个开源项目,支持经典蓝牙 (BR/EDR) 和低功耗蓝牙 (BLE)[^1]。对于开发者来说,掌握如何利用 BlueZ 进行 BLE 应用开发至关重要。 #### 平台环境配置 为了顺利开展基于 BlueZ 的 BLE 开发工作,在开始之前需确保已准备好合适的硬件设备以及软件环境。文中提到使用的具体本为 bluez5.54,并且该库是由 buildroot 编译工具链构建而成。这表明在实际操作前应当熟悉目标系统的交叉编译流程并安装必要的依赖包。 #### 创建 GATT Server 客户端应用实例 针对创建一对能够互相通信的 GATT 服务器和服务客户端的需求,可以参考 `bluez-master/test/example-gatt-client` 及 `bluez-master/test/example-gatt-server` 中提供的 Python 实现方式来编写相应的应用程序逻辑[^2]。下面给出一段简化的服务端代码片段作为示范: ```python import dbus from gi.repository import GLib from example_advertisement import Advertisement from example_gatt_server import Service, Characteristic class DemoService(Service): def __init__(self, bus, index): Service.__init__(self, bus, index, 'org.bluez.example.service', True) self.add_characteristic(DemoCharacteristic(bus, 0, self)) class DemoCharacteristic(Characteristic): def __init__(self, bus, index, service): Characteristic.__init__( self, bus, index, 'org.bluez.example.characteristic', ['read', 'write'], service) def main(): from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() advertisement = Advertisement(bus, 0) obj = bus.get_object('org.bluez', '/org/bluez/hci0') adapter_props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties') demo_service = DemoService(bus, 0) loop = GLib.MainLoop() try: adapter_props.Set('org.bluez.Adapter1', 'Powered', dbus.Boolean(True)) advertisement.register() demo_service.register() loop.run() except Exception as e: print(str(e)) if __name__ == '__main__': main() ``` 上述代码展示了怎样定义一个新的 GATT 服务及其特性,并将其注册到系统总线上以便其他设备发现与连接。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值