4

I am working on a project that connects an Android device with a Raspberry Pi. The RPi needs to be treated like a deployable device that the user never needs to touch. For this reason, I am trying to write a startup batch script on the RPi that will allow the user to pair their Android with the PI.

My idea is that when you startup, this script will run, the user on their phone will try and connect to the RPi, and the RPi will automatically accept this connection.

Here is what I have so far

#!/bin/bash
bluetoothctl -- discoverable on
bluetoothctl -- pairable on
bluetoothctl -- agent on
bluetoothctl -- default-agent

The issue is, when I do it this way I don't get into the [bluetoothctl] prompt that I need to communicate with the Android.

When I run these commands (Without batch script) and try and pair with my Android I get

Request confirmation
[agent] Confirm passkey 861797 (yes/no): yes

And from here I simply need to input yes to instantiate the connection. The issue I'm seeing is 1: I don't know how to stay in the [bluetoothctl] prompt within the command line to communicate with the device and 2: I don't know how to send "Yes" to the prompt.

Again, the important thing for me is that the user never needs to do anything more to the RPi than start it up for deployment purposes. Is there a fix for my problem or perhaps a better way to do it all together?

For those interested, the bluetooth startup connection is in place so that I can send network information to the RPi and it can automatically connect itself to the network so that the main application communication will take place that way.

Here is the desired result of the script which I was able to do manually.

enter image description here

B. Hoeper
  • 216
  • 3
  • 12

2 Answers2

6

Using bluetoothctl in that manner can be problematic as it is not designed to be interactive in that way. As you have put Python as one of the tags, the intended way of accessing this functionality from Python (and other languages) is through the D-Bus API.

These are documented at: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc

And there are examples at: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/test

The confirmation is the RequestConfirmation in the agent API. You can also set discoverable and pairable with the adapter API. Using the API will also allow you to stop discoverable from timing out.

Once the phone has connected, you typically want to mark it as trusted so that it doesn't need to pair again. This is done with the device API.

Below is an example of setting these properties on the adapter with Python. I have left all of the Agent functions in although it is only the RequestConfirmation that is used. I have set it to always agree to whatever code it is sent which is what you asked for in your question.

This example Python script would replace your batch script

import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib

BUS_NAME = 'org.bluez'
ADAPTER_IFACE = 'org.bluez.Adapter1'
ADAPTER_ROOT = '/org/bluez/hci'
AGENT_IFACE = 'org.bluez.Agent1'
AGNT_MNGR_IFACE = 'org.bluez.AgentManager1'
AGENT_PATH = '/my/app/agent'
AGNT_MNGR_PATH = '/org/bluez'
CAPABILITY = 'KeyboardDisplay'
DEVICE_IFACE = 'org.bluez.Device1'
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()

def set_trusted(path):
    props = dbus.Interface(bus.get_object(BUS_NAME, path), dbus.PROPERTIES_IFACE)
    props.Set(DEVICE_IFACE, "Trusted", True)

class Agent(dbus.service.Object):

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

    @dbus.service.method(AGENT_IFACE,
                         in_signature='o', out_signature='s')
    def RequestPinCode(self, device):
        print(f'RequestPinCode {device}')
        return '0000'

    @dbus.service.method(AGENT_IFACE,
                         in_signature="ou", out_signature="")
    def RequestConfirmation(self, device, passkey):
        print("RequestConfirmation (%s, %06d)" % (device, passkey))
        set_trusted(device)
        return

    @dbus.service.method(AGENT_IFACE,
                         in_signature="o", out_signature="")
    def RequestAuthorization(self, device):
        print("RequestAuthorization (%s)" % (device))
        auth = input("Authorize? (yes/no): ")
        if (auth == "yes"):
            return
        raise Rejected("Pairing rejected")

    @dbus.service.method(AGENT_IFACE,
                         in_signature="o", out_signature="u")
    def RequestPasskey(self, device):
        print("RequestPasskey (%s)" % (device))
        set_trusted(device)
        passkey = input("Enter passkey: ")
        return dbus.UInt32(passkey)

    @dbus.service.method(AGENT_IFACE,
                         in_signature="ouq", out_signature="")
    def DisplayPasskey(self, device, passkey, entered):
        print("DisplayPasskey (%s, %06u entered %u)" %
              (device, passkey, entered))

    @dbus.service.method(AGENT_IFACE,
                         in_signature="os", out_signature="")
    def DisplayPinCode(self, device, pincode):
        print("DisplayPinCode (%s, %s)" % (device, pincode))


class Adapter:
    def __init__(self, idx=0):
        bus = dbus.SystemBus()
        self.path = f'{ADAPTER_ROOT}{idx}'
        self.adapter_object = bus.get_object(BUS_NAME, self.path)
        self.adapter_props = dbus.Interface(self.adapter_object,
                                            dbus.PROPERTIES_IFACE)
        self.adapter_props.Set(ADAPTER_IFACE,
                               'DiscoverableTimeout', dbus.UInt32(0))
        self.adapter_props.Set(ADAPTER_IFACE,
                               'Discoverable', True)
        self.adapter_props.Set(ADAPTER_IFACE,
                               'PairableTimeout', dbus.UInt32(0))
        self.adapter_props.Set(ADAPTER_IFACE,
                               'Pairable', True)


if __name__ == '__main__':
    agent = Agent(bus, AGENT_PATH)
    agnt_mngr = dbus.Interface(bus.get_object(BUS_NAME, AGNT_MNGR_PATH),
                               AGNT_MNGR_IFACE)
    agnt_mngr.RegisterAgent(AGENT_PATH, CAPABILITY)
    agnt_mngr.RequestDefaultAgent(AGENT_PATH)

    adapter = Adapter()
    mainloop = GLib.MainLoop()
    try:
        mainloop.run()
    except KeyboardInterrupt:
        agnt_mngr.UnregisterAgent(AGENT_PATH)
        mainloop.quit()
ukBaz
  • 6,985
  • 2
  • 8
  • 31
  • Is there a graceful way to exit the GLib main loop after the device has been trusted and connected? – B. Hoeper Mar 19 '21 at 19:26
  • Much of the Bluetooth API is asynchronous so works best with the event loop of GLib. If you do want to break out of the main loop then use `mainloop.quit()`. You may want to investigate D-Bus signals such as `PropertiesChanged` to add callbacks when things happen/change. – ukBaz Mar 19 '21 at 20:24
  • I see that devices api has a Paired function that returns true or false. Which agent function would I be able to do this check? – B. Hoeper Mar 19 '21 at 21:21
  • I'm trying to add a function like this with no success ```@dbus.service.method(dbus_interface=AGENT_IFACE + ".isPaired", in_signature="", out_signature="") def isPaired(self, device): print("got to is paired") if (device.Paired()): self.MAINLOOP.quit()``` – B. Hoeper Mar 19 '21 at 21:39
  • `Paired` in `org.bluez.Device1` is a property not a method. To step back a little on this... Are you using Bluetooth Classic or BLE? Do you care about Authentication? Would turning off [RequireAuthentication](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/profile-api.txt#n85) be acceptable in your situation? – ukBaz Mar 20 '21 at 12:04
  • I can't get this working and I don't really know how to debug it. No Agent methods appear to ever be called. Can you give a simpler example with NoInputNoOutput so none of the PIN/passkey stuff is needed? – OrangeDog Feb 24 '22 at 15:42
  • To clarify, the example in the question works, but I can't get my version no-pin version working. – OrangeDog Feb 24 '22 at 15:50
  • 1
    @OrangeDog, it probably needs to be new question so there is space to answer it as this is an accepted answer – ukBaz Feb 24 '22 at 15:55
  • @OrangeDog, does the following question help you? https://stackoverflow.com/a/71255445/7721752 – ukBaz Feb 25 '22 at 06:26
  • @ukBaz not really, the pin version works fine. I think this is a bug in bluez 5.55-3.1+rpt1 where NoInputNoOutput just doesn't work. – OrangeDog Feb 25 '22 at 10:13
1

On my raspberry pi 4 with bluez the following will accept my android phones pairing without the need to type anything on the raspberry.

sudo apt install bluez-tools 
sudo bt-agent  -c DisplayOnly -p ~/pins.txt &

pins.txt:

00:00:00:00:00:00 *
*                 *

Note that adding -d to bt-agent did not work for, no matter the -c arguments. Hence the ampersand at the end.

user3817445
  • 455
  • 6
  • 5