Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to Bluez5/DBus based API #404

Open
dogtopus opened this issue Jun 17, 2021 · 6 comments
Open

Update to Bluez5/DBus based API #404

dogtopus opened this issue Jun 17, 2021 · 6 comments
Labels
Linux pinned Don't close automatically

Comments

@dogtopus
Copy link
Collaborator

dogtopus commented Jun 17, 2021

PyBluez is not under active development but we are seeking new contributors
to investigate bugs and submit patches.

System

  • Operating System: Linux
  • Hardware: Any (e.g., Raspberry Pi, external Bluetooth adaptor, etc.)
  • Python Version: 3.x
  • PyBluez Version: Any

Issue

PyBluez still uses the old SDPd API pre-Bluez 5, which is deprecated in favor of DBus-based API since long ago. This causes (at least) the SDP registration to be non-functional on modern Linux systems (can be seen when running e.g. the supplied rfcomm-server.py).

@ghost
Copy link

ghost commented Jul 18, 2021

I'll start working on this

@ghost
Copy link

ghost commented Aug 3, 2021

okay so I'm quite bad at this. It seems that on bluez5 you cannot find services as on bluez4 and that everything related to sdp is done through the org.bluez.Profile1 interface. As I understand, you need to register a Profile when doing the server paper as well as when doing the client paper. Moreover, sockets are created each time a new connection is made. This means that the concept of looking up sdp for a port to connect, or creating a socket before advertising is no longer viable.

In addition, now python sockets support bluetooth without sdp (ej you have to give it a fixed port) which have left BluetoothSocket a bit obsolete .

On the other hand, for running a dbus interface you need a main loop, making every program that uses the module main loop agnostic (or maybe the main loop could be implemented on a thread adding a layer of abstraction )

@ghost
Copy link

ghost commented Aug 3, 2021

I don't understand which approach to take, seems like the most useful would be to expose all the dbus api through a more pythonic way, but for that pydbus is already there, doesn't it? (althought it seems a bit discontinued).

@dogtopus
Copy link
Collaborator Author

dogtopus commented Aug 10, 2021

@vik0t0r There's also dasbus which looks more maintained than pydbus and looks better than dlopen()ing libdbus f*ckery.

Yes BluetoothSocket is officially obsolete on Linux. We can probably just pass it through to AF_BLUETOOTH. Not sure if everything will still work though so maybe proceed with caution. OK forget this part. We can probably still use the BluetoothSocket object for something else (like having a fake socket until the socket is actually created by bluez).

Also I'd prefer a threading implementation unless we suddenly decide to fully move to e.g. asyncio (which probably isn't a good idea).

Also from these I think an API change for how PyBluez handles SDP is probably necessary or it would look pretty ugly. My thought is that we could define a Profile class that somewhat resembles bluez's Profile dbus API, and implement the connection waiting directly on our Profile object. For other platforms we can probably create a socket, block/fire a callback on socket.accept and just pass the socket directly (i.e. the old way but encapsulated in Profile class).

Supporting for asyncio for our Profile API would be a plus since... I like it.

@dogtopus
Copy link
Collaborator Author

dogtopus commented Aug 10, 2021

Hm both dasbus and pydbus might not work because

No support for org.freedesktop.DBus.ObjectManager: There is no support for object managers, however the DBus containers could be a good starting point.

No support for Unix file descriptors: It is not possible to send or receive Unix file descriptors.

EDIT: This is now being tracked at dasbus-project/dasbus#65. Also upon closer look, we might not need ObjectManager implementation.

@ukBaz
Copy link

ukBaz commented Jan 2, 2022

I have been looking at some the issues around Python, D-Bus, and BlueZ. I certainly don't have all the answers and am sharing here in the hope that it will help move things forward for all of us. (Even if it is just to help understand that this isn't a path to go down)

I have seen issues with the various D-Bus Python bindings when trying to use them with BlueZ. As a result of this I have been looking into using the PyGObject bindings that most of the newer D-Bus libraries are built on. My thought was that this might be a better option to explore as the BlueZ D-Bus API requires a small subset of the D-Bus functionality. The other libraries are trying to be generic D-Bus bindings and cover the most widely used functionality.

Below is an experiment I did to create a BlueZ Serial Port Profile (SPP) Server using PyGObject.

import os

from gi.repository import Gio, GLib

# Introspection data for DBus
profile_xml = """
<node>
    <interface name="org.bluez.Profile1">
        <method name="Release"/>
        <method name="NewConnection">
            <arg type="o" name="device" direction="in"/>
            <arg type="h" name="fd" direction="in"/>
            <arg type="a{sv}" name="fd_properties" direction="in"/>
        </method>
        <method name="RequestDisconnection">
            <arg type="o" name="device" direction="in"/>
        </method>
    </interface>
</node>
"""


class DbusService:
    """Class used to publish a DBus service on to the DBus System Bus"""
    def __init__(self, introspection_xml, publish_path):
        self.node_info = Gio.DBusNodeInfo.new_for_xml(introspection_xml).interfaces[0]
        # start experiment
        method_outargs = {}
        method_inargs = {}
        property_sig = {}
        for method in self.node_info.methods:
            method_outargs[method.name] = '(' + ''.join([arg.signature for arg in method.out_args]) + ')'
            method_inargs[method.name] = tuple(arg.signature for arg in method.in_args)
        self.method_inargs = method_inargs
        self.method_outargs = method_outargs

        self.con = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
        self.con.register_object(
            publish_path,
            self.node_info,
            self.handle_method_call,
            self.prop_getter,
            self.prop_setter)

    def handle_method_call(self,
                           connection: Gio.DBusConnection,
                           sender: str,
                           object_path: str,
                           interface_name: str,
                           method_name: str,
                           params: GLib.Variant,
                           invocation: Gio.DBusMethodInvocation
                           ):
        """
        This is the top-level function that handles method calls to
        the server.
        """
        args = list(params.unpack())
        # Handle the case where it is a Unix filedescriptor
        for i, sig in enumerate(self.method_inargs[method_name]):
            if sig == 'h':
                msg = invocation.get_message()
                fd_list = msg.get_unix_fd_list()
                args[i] = fd_list.get(args[i])
        func = self.__getattribute__(method_name)
        result = func(*args)
        if result is None:
            result = ()
        else:
            result = (result,)
        outargs = ''.join([_.signature
                           for _ in invocation.get_method_info().out_args])
        send_result = GLib.Variant(f'({outargs})', result)
        invocation.return_value(send_result)

    def prop_getter(self,
                    connection: Gio.DBusConnection,
                    sender: str,
                    object: str,
                    iface: str,
                    name: str):
        """Return requested values on DBus from Python object"""
        py_value = self.__getattribute__(name)
        signature = self.node_info.lookup_property(name).signature
        if py_value:
            return GLib.Variant(signature, py_value)
        return None

    def prop_setter(self,
                    connection: Gio.DBusConnection,
                    sender: str,
                    object: str,
                    iface: str,
                    name: str,
                    value: GLib.Variant):
        """Set values on Python object from DBus"""
        self.__setattr__(name, value.unpack())
        return True


class Profile(DbusService):

    def __init__(self, introspection_xml, publish_path):
        super().__init__(introspection_xml, publish_path)
        self.fd = -1

    def Release(self):
        print('Release')

    def NewConnection(self, path, fd, properties):
        self.fd = fd
        print(f'NewConnection({path}, {self.fd}, {properties})')
        for key in properties.keys():
            if key == 'Version' or key == 'Features':
                print('  %s = 0x%04x' % (key, properties[key]))
            else:
                print('  %s = %s' % (key, properties[key]))
        io_id = GLib.io_add_watch(self.fd,
                                  GLib.PRIORITY_DEFAULT,
                                  GLib.IO_IN | GLib.IO_PRI,
                                  self.io_cb)

    def io_cb(self, fd, conditions):
        data = os.read(fd, 1024)
        print('Callback Data: {0}'.format(data.decode('ascii')))
        os.write(fd, bytes(list(reversed(data.rstrip()))) + b'\n')
        return True

    def RequestDisconnection(self, path):
        print('RequestDisconnection(%s)' % (path))
        if self.fd > 0:
            os.close(self.fd)
            self.fd = -1


def main():
    obj_mngr = Gio.DBusObjectManagerClient.new_for_bus_sync(
        bus_type=Gio.BusType.SYSTEM,
        flags=Gio.DBusObjectManagerClientFlags.NONE,
        name='org.bluez',
        object_path='/',
        get_proxy_type_func=None,
        get_proxy_type_user_data=None,
        cancellable=None,
    )

    manager = obj_mngr.get_object('/org/bluez').get_interface('org.bluez.ProfileManager1')
    adapter = obj_mngr.get_object('/org/bluez/hci0').get_interface('org.freedesktop.DBus.Properties')
    mainloop = GLib.MainLoop()

    discoverable = adapter.Get('(ss)', 'org.bluez.Adapter1', 'Discoverable')

    if not discoverable:
        print('Making discoverable...')
        adapter.Set('(ssv)', 'org.bluez.Adapter1',
                    'Discoverable', GLib.Variant.new_boolean(True))

    profile_path = '/org/bluez/test/profile'
    server_uuid = '00001101-0000-1000-8000-00805f9b34fb'
    opts = {
        'Version': GLib.Variant.new_uint16(0x0102),
        'AutoConnect': GLib.Variant.new_boolean(True),
        'Role': GLib.Variant.new_string('server'),
        'Name': GLib.Variant.new_string('SerialPort'),
        'Service': GLib.Variant.new_string('00001101-0000-1000-8000-00805f9b34fb'),
        'RequireAuthentication': GLib.Variant.new_boolean(False),
        'RequireAuthorization': GLib.Variant.new_boolean(False),
        'Channel': GLib.Variant.new_uint16(1),
    }

    print('Starting Serial Port Profile...')

    profile = Profile(profile_xml, profile_path)

    manager.RegisterProfile('(osa{sv})', profile_path, server_uuid, opts)

    try:
        mainloop.run()
    except KeyboardInterrupt:
        mainloop.quit()


if __name__ == '__main__':
    main()

This was a proof-of-concept to see if it was possible. I haven't done a lot of testing with it and I certainly make no claims that it is ready for production use or the required structure for use in the library.

This was the client I used to test it with:

import socket

server_address = "xx:xx:xx:xx:xx:xx"
server_port = 1
with socket.socket(socket.AF_BLUETOOTH,
                   socket.SOCK_STREAM,
                   socket.BTPROTO_RFCOMM) as c:

    c.connect((server_address, server_port))
    c.send(b"desserts")
    print(c.recv(1024).decode())

@dogtopus dogtopus pinned this issue Apr 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Linux pinned Don't close automatically
Projects
None yet
Development

No branches or pull requests

2 participants