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)
This simply runs the agent in such a way that it will accept any and all pairing requests and authorise them.
2)
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
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
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
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
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>