0
votes

I've started a project where I need to actively (all the time) scan for BLE Devices. I'm on Linux, using Bluez 5.49 and I use Python to communicate with dbus 1.10.20). I' m able to start scanning, stop scanning with bluetoothctl and get the BLE Advertisement data through DBus (GetManagedObjects() of the BlueZ interface). The problem I have is when I let the scanning for many hours, dbus-deamon start to take more and more of the RAM and I'm not able to find how to "flush" what dbus has gathered from BlueZ. Eventually the RAM become full and Linux isn't happy.

So I've tried not to scan for the entire time, that would maybe let the Garbage collector do its cleanup. It didn't work.

I've edited the /etc/dbus-1/system.d/bluetooth.conf to remove any interface that I didn't need

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

That has slow down the RAM build-up but didn't solve the issue.

I've found a way to inspect which connection has byte waiting and confirmed that it comes from blueZ

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

and lastly, I've found that someone needs to read what is waiting in DBus in order to free the memory. So I've found this : https://stackoverflow.com/a/60665430/15325057

And I receive the data that BlueZ is sending over but the memory still built-up.

The only way I know to free up dbus is to reboot linux. which is not ideal.

I'm coming at the end of what I understand of DBus and that's why I'm here today. If you have any insight that could help me to free dbus from BlueZ messages, it would be highly appreciated.

Thanks in advance

EDIT Adding the DBus code i use to read the discovered devices:

#!/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...] 
2
How are you measuring the memory build up? I have left my RPi (BlueZ 5.50) scanning for about an hour and I have not seen the memory filling up. I've used watch -n20 free -m to track it. I do a RemoveDevice after I have discovered them but that is because of the duplicate data issue not memory. What does your code look like? Could it be your GetManagedObjects command is building a bigger and bigger list?ukBaz
@ukBaz I've added the code I'm using to read the discovered devices. Just to be sure it's not the python code that take more space in RAM it's dbus-daemon. I'm using "top" to monitor dbus-daemon and i check the "RES" column. For example for a day of scanning dbus-daemon is at 48196 Kb of reserved memoryuser15325057
@ukBaz well, you might be right for the growing of the GetManagedObjects. my script is about the same size of the dbus-daemon. But still how do i free-up DBus in the first place ? I don't need the data that in there anymore, I just want to make a new survey of my environement. 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 Scanner.pyuser15325057

2 Answers

2
votes

Indeed, Bluez flushes memory when you stop discovering. So in order to scan continuously you need start and stop the discovery all the time. I discover for 6 seconds, wait 1 second and then start discovering for 6 seconds again...and so on. If you check the logs you will see it deletes a lot of stuff when stopping discovery.

0
votes

I can't really reproduce your error exactly but my system is not happy running that fast while loop repeatedly getting the data from GetManagedObjects. Below is the code I ran based on your code with a little bit of refactoring...

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()

I'm not sure what you are trying to do in the broader project but if I can make some recommendations...

A more typical way of scanning for service/manufacturer data is to subscribe to signals in D-Bus that trigger callbacks when something of interest happens.

Below is some code I use to look for iBeacons and Eddystone beacons. This runs using the GLib event loop which is maybe something you have ruled out but is more efficient on resources.

It does use different Python dbus bindings as I find pydbus more "pythonic".

I have left the code in processing the beacons as it might be a useful reference.

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\t{beacon_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'0000{uuid_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()