Thank you for your donation!


Add bluetooth pairing agent
#1
Moode currently lacks a pairing agent. This makes pairing new devices clumsy because the default behaviour of bluez is to accept pairing requests  but don't authorise them. The current crudge to work around this is, that the scan script authorizes all devices it finds.
As a result, to pair a device the first time, one has to do an odd sequence of pairing, scanning and reconnecting, ... until it eventually all works.

Usually, a bluetooth agent is in charge to handle incoming pairing requests and authorise them where appropriate.

I've written a simple agent that is able to do this. It is derived from the sample agent that ships with the bluez sources, thus I put it under GPL2 (same as bluez).

There are two useful ways of running the agent:

1)
Code:
sudo ./moode-bluetooth-agent.py --agent --disable_pair_mode_switch --pair_mode

This simply runs the agent in such a way that it will accept any and all pairing requests and authorise them.

2)
Code:
sudo ./moode-bluetooth-agent.py --agent
This will start the agent in a mode that will reject all pairing requests by default. However, at the same time, it registers itself as a dbus service and waits to be instructed to enter pairing mode. Calling
Code:
sudo ./moode-bluetooth-agent.py --pair_mode --timeout 30

will activate pairing mode for 30 seconds.

The idea of the second mode is, that pairing is disabled by default, but the gui offers a "pair" button to temporarily activate it - similarly how many commercial bluetooth devices do it.

It is also possible to instruct a running agent to activate pairing indefinitely (don't give a --timeout) or to disable pairing (--disable_pair_mode). Thus, the state of a running agent can be controlled completely without needing to restart it.


My suggestion would be to integrate the agent in such a way that it always runs. The user can then choose whether pairing should be always open or only open upon pressing the "pair" button. The gui would then change the mode of the running agent accordingly.

Until such time that the gui supports this, option 1) might be used as a default to open enable pairing for all.

If you launch the agent from a start up script, you might want to add --wait_for_bluez. The reason is, that it takes quite a while until buetoothd is available after system boot. With this option, the agent will retry to make the dbus connection instead if failing with an error.


The agent itself is a single python script:

./moode-bluetooth-agent.py
Code:
#!/usr/bin/python
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License version 2 as
#    published by the Free Software Foundation.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.
#


from __future__ import absolute_import, print_function, unicode_literals

from optparse import OptionParser
import sys
import time
import dbus
import dbus.service
import dbus.mainloop.glib
try:
  from gi.repository import GObject
except ImportError:
  import gobject as GObject



BUS_NAME = 'org.bluez'
AGENT_INTERFACE = 'org.bluez.Agent1'
AGENT_PATH = "/moode/agent"
WELL_KNOWN_NAME = 'org.moodeaudio.bluez.agent'
PAIR_MODE_INTERFACE = 'org.moodeaudio.bluez.agent.PairMode'



mainloop = None


class Rejected(dbus.DBusException):
    _dbus_error_name = "org.bluez.Error.Rejected"

class Cancelled(dbus.DBusException):
    _dbus_error_name = "org.bluez.Error.Canceled"


class Agent(dbus.service.Object):

    def __init__(self, conn, object_path, bus_name):
        super(Agent, self).__init__(conn, object_path, bus_name)
        self.pair_mode_active_until = -float('inf')

    @property
    def pair_mode_active(self):
        return time.time() <= self.pair_mode_active_until



    @dbus.service.method(PAIR_MODE_INTERFACE, in_signature="d", out_signature="")
    def ActivatePairMode(self, timeout):
        print("ActivatePairMode called with %s" % (timeout,))
        if timeout > 0:
            self.pair_mode_active_until = time.time() + timeout
        else:
            self.pair_mode_active_until = -float('inf')


    @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="")
    def Release(self):
        print("Release")
        if mainloop is not None:
            mainloop.quit()

    @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="")
    def AuthorizeService(self, device, uuid):
        if self.pair_mode_active:
            print("Authorizing service (%s, %s)" % (device, uuid))
            return
        else:
            raise Rejected("Pair mode not activated.")

    @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="s")
    def RequestPinCode(self, device):
        raise Cancelled("Pin code not supported")

    @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="u")
    def RequestPasskey(self, device):
        raise Cancelled("Passkey code not supported")


    @dbus.service.method(AGENT_INTERFACE, in_signature="ouq", out_signature="")
    def DisplayPasskey(self, device, passkey, entered):
        raise Cancelled("Passkey code not supported")

    @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="")
    def DisplayPinCode(self, device, pincode):
        raise Cancelled("Pin code not supported")

    @dbus.service.method(AGENT_INTERFACE, in_signature="ou", out_signature="")
    def RequestConfirmation(self, device, passkey):
        raise Cancelled("Confirmation not supported")

    @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="")
    def RequestAuthorization(self, device):
        if self.pair_mode_active:
            print("Authorizing device %s" % (device))
            return
        else:
            raise Rejected("Pair mode not activated.")

    @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="")
    def Cancel(self):
        print("Cancel")



if __name__ == '__main__':


    parser = OptionParser()
    parser.add_option("-a", "--agent", action="store_true", dest="agent", help="Run as Bluetooth agent. If not given, a running agent is contacted to change the pairing mode.")
    parser.add_option("-w", "--wait_for_bluez", action="store_true", dest="wait_for_bluez", help="During system startup, bluez might not yet be available. this option causes the agent to re-try repeatedly until bluez has started.")
    parser.add_option("-p", "--pair_mode", action="store_true", dest="pair_mode", help="Activate pairing mode.")
    parser.add_option("-t", "--timeout", action="store", type="int", dest="timeout", help="Timeout in seconds for pairing mode. If not given, pairing mode will be active indefinitely.")
    parser.add_option("-d", "--disable_pair_mode", action="store_true", dest="disable_pair_mode", help="Disable pairing mode.")
    parser.add_option("-s", "--disable_pair_mode_switch", action="store_true", dest="disable_pair_mode_switch", help="Don't register a well known name with the dbus. This disables switching pairing mode from another process. Use it if the necessary dbus permissions aren't set up.")

    (options, args) = parser.parse_args()

    if options.pair_mode and options.disable_pair_mode:
        print("The options pair_mode and disable_pair_mode are mutally exclusive.")
        sys.exit()


    if options.agent:

        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

        bus = dbus.SystemBus()


        if options.disable_pair_mode_switch:
            bus_name = None
        else:
            bus_name = dbus.service.BusName(WELL_KNOWN_NAME, bus)

        agent = Agent(bus, AGENT_PATH, bus_name)

        if options.pair_mode:
            if options.timeout is not None:
                timeout = options.timeout
            else:
                timeout = float('inf')
            agent.ActivatePairMode(timeout)

        mainloop = GObject.MainLoop()

        while True:
            try:
                obj = bus.get_object(BUS_NAME, "/org/bluez");
            except dbus.exceptions.DBusException:
                if options.wait_for_bluez:
                    time.sleep(1)
                else:
                    raise
            else:
                break
                
        manager = dbus.Interface(obj, "org.bluez.AgentManager1")

        manager.RegisterAgent(AGENT_PATH, "NoInputNoOutput")
        print("Agent registered")

        manager.RequestDefaultAgent(AGENT_PATH)
        print("Agent registered as default agent.")

        mainloop.run()

    else:
        bus = dbus.SystemBus()
        obj = bus.get_object(WELL_KNOWN_NAME, AGENT_PATH)
        other_agent = dbus.Interface(obj, PAIR_MODE_INTERFACE)

        if options.pair_mode:
            if options.timeout is not None:
                timeout = options.timeout
            else:
                timeout = float('inf')

            other_agent.ActivatePairMode(float(timeout))

        if options.disable_pair_mode:
            other_agent.ActivatePairMode(float('-inf'))



For the second mode, dbus permissions must be set up so the agent is allowed to register a well-known name for receiving the pair mode switch commands. If the permissions aren't set up correctly, --disable_pair_mode_switch must be given, otherwise the agent terminates with an error.

/etc/dbus-1/system.d/moode.conf
Code:
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>

 <!-- ../system.conf have denied everything, so we just punch some holes -->

 <policy user="root">
   <allow own="org.moodeaudio.bluez.agent"/>
   <allow send_destination="org.moodeaudio.bluez.agent"/>
   <allow send_interface="org.moodeaudio.bluez.agent.PairMode"/>
 </policy>

 <!-- allow users of bluetooth group to communicate -->
 <policy group="bluetooth">
   <allow send_destination="org.moodeaudio.bluez.agent"/>
 </policy>

 <policy at_console="true">
   <allow send_destination="org.moodeaudio.bluez.agent"/>
 </policy>

 <!-- allow users of lp group (printing subsystem) to
      communicate with bluetoothd -->
 <policy group="lp">
   <allow send_destination="org.moodeaudio.bluez.agent"/>
 </policy>

 <policy context="default">
   <deny send_destination="org.moodeaudio.bluez.agent"/>
 </policy>

</busconfig>
Reply
#2
@kaymes

You discussed a problem and didn't get satisfaction; researched it; developed a clean solution; and documented it so even a dilettante like me can understand it. That's a big +1 in my book!

I won't be able to work with it until tonight. It's up to @Tim Curtis to decide what to do with it.

Regards,
Kent
Reply
#3
Is it a replacement for the SCAN function?
Reply
#4
For incoming connections, you won't need to use SCAN any more. New devices can simply connect to moode whenever the agent is in pair mode. (paired devices can connect at any time regardless of pair mode).
Reply
#5
Worked perfectly to pair and authorize my iPhone :-)

Code:
pi@rp3:~ $ sudo /var/www/command/bt-agent.py --agent --disable_pair_mode_switch --pair_mode
ActivatePairMode called with inf
Agent registered
Agent registered as default agent.
Authorizing service (/org/bluez/hci0/dev_08_F6_9C_8F_6B_C5, 0000110d-0000-1000-8000-00805f9b34fb)

I'll integrate this into the UI for the next beta patch set so we can get some field testing.

-Tim
Reply
#6
I'm not sure I fully understand the polling loops in the code and how often they poll. I'd like to be able to simply run this in the background continuously as part of turning on the Bluetooth renderer as long as it's efficient in it's use of polling loops.
Reply
#7
Pairing agent integrated. This will be available in the next patch set for moOde 5 beta :-)

   
Reply
#8
The agent has been running continuously on my PI for over 38 hours now. "top" shows that it used 0.49 seconds of CPU time all up. Thus, there is no problem with wasting CPU time when idle.
Reply
#9
Similar results on my end.

We still have the education challenge of explaining the two ways that moOde Bluetooth operates i.e incoming requests to pair/connect which are now automated via your code vs outgoing pairings/connections to for example Bluetooth speakers which require the existing BlueZ SCAN process.

-Tim
Reply
#10
Outgoing connections are handled via the same dbus interface. In fact, the script that I turned into the agent also contains code to do outgoing pair requests. However, I removed all of that for the agent I posted. Have a look at test/simple-agent in the bluez sources (https://git.kernel.org/pub/scm/bluetooth/bluez.git).

The part that seems missing is a method to show all devices that are currently in range for easy selection. But if you look at the implementation of find_device in bluezutils.py, it seems, that it is merely a matter of inspecting what get_managed_objects returns. The APIs are documented in the doc folder, so you can look at how the dbus methods are meant to be used.
Reply


Forum Jump: