主动 BLE 扫描 (BlueZ) - DBus 问题

Posted

技术标签:

【中文标题】主动 BLE 扫描 (BlueZ) - DBus 问题【英文标题】:Active BLE Scanning (BlueZ) - Issue with DBus 【发布时间】:2021-06-02 12:50:49 【问题描述】:

我已经启动了一个项目,我需要主动(一直)扫描 BLE 设备。我在 Linux 上,使用 Bluez 5.49,我使用 Python 与 dbus 1.10.20 进行通信)。 我能够开始扫描,使用 bluetoothctl 停止扫描并通过 DBus(BlueZ 接口的 GetManagedObjects())获取 BLE 广告数据。我遇到的问题是当我让扫描数小时时,dbus-deamon 开始占用越来越多的 RAM,我无法找到如何“刷新”dbus 从 BlueZ 收集的内容。最终 RAM 变满了,Linux 不高兴了。

所以我一直尝试不扫描,这可能会让垃圾收集器进行清理。没用。

我已编辑 /etc/dbus-1/system.d/bluetooth.conf 以删除任何我不需要的接口

<policy user="root">
    <allow own="org.bluez"/>
    <allow send_destination="org.bluez"/>
</policy>

这减缓了 RAM 的积累,但并没有解决问题。

我找到了一种方法来检查哪个连接有字节等待并确认它来自 blueZ

Connection :1.74 with pid 3622 '/usr/libexec/bluetooth/bluetoothd --experimental ' (org.bluez):
        IncomingBytes=1253544
        PeakIncomingBytes=1313072
        OutgoingBytes=0
        PeakOutgoingBytes=210

最后,我发现有人需要读取 DBus 中等待的内容才能释放内存。所以我发现了这个:https://***.com/a/60665430/15325057

我收到了 BlueZ 发送过来的数据,但内存仍在增加。

我知道释放 dbus 的唯一方法是重新启动 linux。这并不理想。

我对 DBus 的了解即将结束,这就是我今天来到这里的原因。 如果您有任何见解可以帮助我将 dbus 从 BlueZ 消息中解放出来,我们将不胜感激。

提前致谢

编辑添加我用来读取发现设备的 DBus 代码:

#!/usr/bin/python3

import dbus

BLUEZ_SERVICE_NAME = "org.bluez"
DBUS_OM_IFACE = "org.freedesktop.DBus.ObjectManager"
DEVICES_IFACE = "org.bluez.Device1"

def main_loop(subproc):
    devinfo = None
    objects = None

    dbussys = dbus.SystemBus()
    dbusconnection = dbussys.get_object(BLUEZ_SERVICE_NAME, "/")
    bluezInterface = dbus.Interface(dbusconnection, DBUS_OM_IFACE)
    
        
    while True:
        try:
            objects = bluezInterface.GetManagedObjects()
        except dbus.DBusException as err:
            print("dbus Error : " + str(err))
            pass

        all_devices = (str(path) for path, interfaces in objects.items() if DEVICES_IFACE in interfaces.keys())

        for path, interfaces in objects.items():
            if "org.bluez.Adapter1" not in interfaces.keys():
                continue

            device_list = [d for d in all_devices if d.startswith(path + "/")]

            for dev_path in device_list:
                properties = objects[dev_path][DEVICES_IFACE]
                
                if "ServiceData" in properties.keys() and "Name" in properties.keys() and "RSSI" in properties.keys():
                    #[... Do someting...] 

【问题讨论】:

你是如何测量内存积累的?我已经离开我的 RPi (BlueZ 5.50) 扫描了大约一个小时,我还没有看到内存填满。我用watch -n20 free -m 跟踪它。在我发现它们之后我做了一个RemoveDevice,但那是因为重复数据问题而不是内存。你的代码是什么样的?会不会是你的GetManagedObjects 命令正在构建一个越来越大的列表? @ukBaz 我添加了用于读取已发现设备的代码。只是为了确保在 RAM 中占用更多空间的不是 python 代码,而是 dbus-daemon。我正在使用“top”来监控 dbus-daemon 并检查“RES”列。例如,扫描 dbus-daemon 一天的保留内存为 48196 Kb @ukBaz 好吧,您可能适合 GetManagedObjects 的发展。我的脚本与 dbus-daemon 的大小差不多。但是我仍然如何首先释放 DBus 呢?我不再需要那里的数据,我只想对我的环境进行一次新的调查。 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 280 message+ 20 0 49892 48196 2524 S 0.6 9.5 3:54.27 dbus-daemon 3692 root 20 0 46368 40408 6356 S 0.0 8.0 27:43.39 扫描仪。 【参考方案1】:

确实,当您停止发现时,Bluez 会刷新内存。因此,为了连续扫描,您需要始终启动和停止发现。我发现 6 秒,等待 1 秒,然后再次开始发现 6 秒……以此类推。如果您检查日志,您会看到它在停止发现时删除了很多内容。

【讨论】:

是的,我看到了,但 Dbus 没有释放。我尝试了 10 秒发现和 30 秒关闭,dbus-daemon 仍在 RAM 中增长,只是慢得多 您可能想要升级您的 Bluez 版本。自您使用的版本以来,已经有几次与内存泄漏有关的提交。【参考方案2】:

我无法真正准确地重现您的错误,但我的系统不满意运行那么快的 while 循环反复从 GetManagedObjects 获取数据。 以下是我根据您的代码运行的代码,并进行了一些重构...

import dbus

BLUEZ_SERVICE_NAME = "org.bluez"
DBUS_OM_IFACE = "org.freedesktop.DBus.ObjectManager"
ADAPTER_IFACE = "org.bluez.Adapter1"
DEVICES_IFACE = "org.bluez.Device1"

def main_loop():
    devinfo = None
    objects = None

    dbussys = dbus.SystemBus()
    dbusconnection = dbussys.get_object(BLUEZ_SERVICE_NAME, "/")
    bluezInterface = dbus.Interface(dbusconnection, DBUS_OM_IFACE)

    while True:
        objects = bluezInterface.GetManagedObjects()
        for path in objects:
            name = objects[path].get(DEVICES_IFACE, ).get('Name')
            rssi = objects[path].get(DEVICES_IFACE, ).get('RSSI')
            service_data = objects[path].get(DEVICES_IFACE, ).get('ServiceData')
            if all((name, rssi, service_data)):
                print(f'name @ rssi = service_data')
                #[... Do someting...] 

if __name__ == '__main__':
    main_loop()

我不确定你想在更广泛的项目中做什么,但如果我可以提出一些建议......

扫描服务/制造商数据的更典型方法是订阅 D-Bus 中的信号,当感兴趣的事情发生时触发回调。

以下是我用来查找 iBeacons 和 Eddystone 信标的一些代码。这使用 GLib 事件循环运行,这可能是您已排除但在资源上更有效的东西。

它确实使用了不同的 Python dbus 绑定,因为我发现 pydbus 更“pythonic”。

我保留了处理信标的代码,因为它可能是一个有用的参考。

import argparse
from gi.repository import GLib
from pydbus import SystemBus
import uuid

DEVICE_INTERFACE = 'org.bluez.Device1'

remove_list = set()


def stop_scan():
    """Stop device discovery and quit event loop"""
    adapter.StopDiscovery()
    mainloop.quit()


def clean_beacons():
    """
    BlueZ D-Bus API does not show duplicates. This is a
    workaround that removes devices that have been found
    during discovery
    """
    not_found = set()
    for rm_dev in remove_list:
        try:
            adapter.RemoveDevice(rm_dev)
        except GLib.Error as err:
            not_found.add(rm_dev)
    for lost in not_found:
        remove_list.remove(lost)


def process_eddystone(data):
    """Print Eddystone data in human readable format"""
    _url_prefix_scheme = ['http://www.', 'https://www.',
                          'http://', 'https://', ]
    _url_encoding = ['.com/', '.org/', '.edu/', '.net/', '.info/',
                     '.biz/', '.gov/', '.com', '.org', '.edu',
                     '.net', '.info', '.biz', '.gov']
    tx_pwr = int.from_bytes([data[1]], 'big', signed=True)
    # Eddystone UID Beacon format
    if data[0] == 0x00:
        namespace_id = int.from_bytes(data[2:12], 'big')
        instance_id = int.from_bytes(data[12:18], 'big')
        print(f'\t\tEddystone UID: namespace_id - instance_id \u2197 tx_pwr')
    # Eddystone URL beacon format
    elif data[0] == 0x10:
        prefix = data[2]
        encoded_url = data[3:]
        full_url = _url_prefix_scheme[prefix]
        for letter in encoded_url:
            if letter < len(_url_encoding):
                full_url += _url_encoding[letter]
            else:
                full_url += chr(letter)
        print(f'\t\tEddystone URL: full_url \u2197 tx_pwr')


def process_ibeacon(data, beacon_type='iBeacon'):
    """Print iBeacon data in human readable format"""
    print('DATA:', data)
    beacon_uuid = uuid.UUID(bytes=bytes(data[2:18]))
    major = int.from_bytes(bytearray(data[18:20]), 'big', signed=False)
    minor = int.from_bytes(bytearray(data[20:22]), 'big', signed=False)
    tx_pwr = int.from_bytes([data[22]], 'big', signed=True)
    print(f'\t\tbeacon_type: beacon_uuid - major - minor \u2197 tx_pwr')


def ble_16bit_match(uuid_16, srv_data):
    """Expand 16 bit UUID to full 128 bit UUID"""
    uuid_128 = f'0000uuid_16-0000-1000-8000-00805f9b34fb'
    return uuid_128 == list(srv_data.keys())[0]


def on_iface_added(owner, path, iface, signal, interfaces_and_properties):
    """
    Event handler for D-Bus interface added.
    Test to see if it is a new Bluetooth device
    """
    iface_path, iface_props = interfaces_and_properties
    if DEVICE_INTERFACE in iface_props:
        on_device_found(iface_path, iface_props[DEVICE_INTERFACE])


def on_device_found(device_path, device_props):
    """
    Handle new Bluetooth device being discover.
    If it is a beacon of type iBeacon, Eddystone, AltBeacon
    then process it
    """
    address = device_props.get('Address')
    address_type = device_props.get('AddressType')
    name = device_props.get('Name')
    alias = device_props.get('Alias')
    paired = device_props.get('Paired')
    trusted = device_props.get('Trusted')
    rssi = device_props.get('RSSI')
    service_data = device_props.get('ServiceData')
    manufacturer_data = device_props.get('ManufacturerData')
    if address.casefold() == '00:c3:f4:f1:58:69':
        print('Found mac address of interest')
    if service_data and ble_16bit_match('feaa', service_data):
        process_eddystone(service_data['0000feaa-0000-1000-8000-00805f9b34fb'])
        remove_list.add(device_path)
    elif manufacturer_data:
        for mfg_id in manufacturer_data:
            # iBeacon 0x004c
            if mfg_id == 0x004c and manufacturer_data[mfg_id][0] == 0x02:
                process_ibeacon(manufacturer_data[mfg_id])
                remove_list.add(device_path)
            # AltBeacon 0xacbe
            elif mfg_id == 0xffff and manufacturer_data[mfg_id][0:2] == [0xbe, 0xac]:
                process_ibeacon(manufacturer_data[mfg_id], beacon_type='AltBeacon')
                remove_list.add(device_path)
    clean_beacons()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--duration', type=int, default=0,
                        help='Duration of scan [0 for continuous]')
    args = parser.parse_args()
    bus = SystemBus()
    adapter = bus.get('org.bluez', '/org/bluez/hci0')

    bus.subscribe(iface='org.freedesktop.DBus.ObjectManager',
                  signal='InterfacesAdded',
                  signal_fired=on_iface_added)

    mainloop = GLib.MainLoop()


    if args.duration > 0:
        GLib.timeout_add_seconds(args.duration, stop_scan)
    adapter.SetDiscoveryFilter('DuplicateData': GLib.Variant.new_boolean(False))
    adapter.StartDiscovery()

    try:
        print('\n\tUse CTRL-C to stop discovery\n')
        mainloop.run()
    except KeyboardInterrupt:
        stop_scan()

【讨论】:

感谢@ukBaz 成功了。 Dbus 现在稳定在 3.6 Mb 左右。不知道为什么 Dbus 增长得这么快,但是与你的脚本异步工作就好了。非常感谢您的帮助:)

以上是关于主动 BLE 扫描 (BlueZ) - DBus 问题的主要内容,如果未能解决你的问题,请参考以下文章

使用 bluez 5.43 和 DBus 从 BLE 传感器读取广告数据包的正确方法是啥

BlueZ DBUS API - GATT 接口对 BLE 设备不可用

BlueZ D-Bus C,应用程序BLE

无法取消注册 ble 通知的 dbus 信号

BlueZ 显示 DBUS 上的旧缓存数据

从 Bluez 5.48 开始,iPhone 在连接 BLE GAP 外围设备时需要配对,为啥?