MiSTer-devel / Main_MiSTer

Main MiSTer binary and Wiki
GNU General Public License v3.0
3.01k stars 324 forks source link

Bluetooth controller pairing fails when there are multiple devices present #215

Closed timherr closed 4 years ago

timherr commented 4 years ago

I am having trouble connecting a bluetooth controller when there are multiple devices nearby:

/usr/sbin# hcitool scan
Scanning ...
        1C:96:5A:81:6F:E9       Wireless Controller
        CC:6D:A0:26:E9:52       Roku Player
/usr/sbin# ./btpair
Switch input device
to pairing mode.
searching...

./btpair: line 8: [: 1C:96:5A:81:6F:E9: binary operator expected
nothing found.

Unplugging my Roku allows the controller to pair perfectly. I suspect the btpair script isn't handling the newline in the $MAC variable appropriately, but I'll leave that to the experts to confirm. :)

timherr commented 4 years ago

Looks like another user reported a similar problem here, but closed the ticket because it wasn't their primary issue: https://github.com/MiSTer-devel/Main_MiSTer/issues/151#issuecomment-565283023

timherr commented 4 years ago

Looks like there are two issues in btpair:

  1. Newlines in $MAC aren't handled gracefully
  2. The script will fail if there are multiple devices available, even if we account for the newline

Quick and dirty fixes for each:

  1. Put double quotes around ${MACS} when checking for null value
  2. Include a loop to attempt to connect to multiple devices

Here's a naive update that will attempt to connect to every device. In my case, it works because the connection to the Roku fails (which I don't want to connect, anyway) and the connection to the controller succeeds.

#!/bin/sh

echo Switch input device
echo to pairing mode.
echo searching...
echo
MACS=$(hcitool scan --flush | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}')
IFS='
'
if [ ! -z "${MACS}" ]; then
        for MAC in $MACS
        do
                echo Got device: $MAC
                echo Pairing...
                echo
                pair-agent hci0 $MAC
                echo Add to trust list...
                MAC=$(echo $MAC | sed -e 's/:/_/g')
                BTADAPTER=`dbus-send --system --dest=org.bluez --print-reply / org.bluez.Manager.DefaultAdapter | tail -1 | sed 's/^.*"\(.*\)".*$/\1/'`
                dbus-send --system --dest=org.bluez --print-reply $BTADAPTER/dev_$MAC org.bluez.Device.SetProperty string:Trusted variant:boolean:true >/dev/nul
                echo Connecting...
                dbus-send --system --dest=org.bluez --print-reply $BTADAPTER/dev_$MAC org.bluez.Input.Connect >/dev/nul
                echo Done.
        done
else
        echo nothing found.
fi
c0d3h4x0r commented 4 years ago

I’ve noticed the same symptoms sometimes; no idea if it’s the same root cause, though. I’ll try out your script when I get a chance.

Note: better to do IFS=$’\n’ for readability.

ChloeMarieTaylor76 commented 4 years ago

Had the same symptoms, I think the macs of multiple devices were been merged into one long string. The console seemed to suggest as much. I managed to muddle through with the help of tim's script above, but it is so hit and miss. I 9 times out of 10 get this error

C:\Users\Chloe>ssh root@192.168.1.133 Password: /root# cd /media/fat/scripts /media/fat/scripts# bluetooth_pair.sh Switch input device to pairing mode. searching...

Got device: 77:01:09:02:25:EB Pairing...

Traceback (most recent call last): File "/sbin/pair-agent", line 117, in path = manager.FindAdapter(args[0]) File "/usr/lib/python3.5/site-packages/dbus/proxies.py", line 70, in call File "/usr/lib/python3.5/site-packages/dbus/proxies.py", line 145, in call File "/usr/lib/python3.5/site-packages/dbus/connection.py", line 651, in call_blocking dbus.exceptions.DBusException: org.bluez.Error.NoSuchAdapter: No such adapter Add to trust list... Error org.bluez.Error.NoSuchAdapter: No such adapter Error org.freedesktop.DBus.Error.UnknownObject: Method "SetProperty" with signature "sv" on interface "org.bluez.Device" doesn't exist

Connecting... Error org.freedesktop.DBus.Error.UnknownObject: Method "Connect" with signature "" on interface "org.bluez.Input" doesn't exist

Done. Got device: E4:17:D8:03:05:79 Pairing...

Traceback (most recent call last): File "/sbin/pair-agent", line 117, in path = manager.FindAdapter(args[0]) File "/usr/lib/python3.5/site-packages/dbus/proxies.py", line 70, in call File "/usr/lib/python3.5/site-packages/dbus/proxies.py", line 145, in call File "/usr/lib/python3.5/site-packages/dbus/connection.py", line 651, in call_blocking dbus.exceptions.DBusException: org.bluez.Error.NoSuchAdapter: No such adapter Add to trust list... Error org.bluez.Error.NoSuchAdapter: No such adapter Error org.freedesktop.DBus.Error.UnknownObject: Method "SetProperty" with signature "sv" on interface "org.bluez.Device" doesn't exist

Connecting... Error org.freedesktop.DBus.Error.UnknownObject: Method "Connect" with signature "" on interface "org.bluez.Input" doesn't exist

Done. /media/fat/scripts#

c0d3h4x0r commented 4 years ago

Using Tim's start as inspiration, I worked on this for a few hours this evening and got it all working well. I'm working on creating a proper pull request of the fix.

c0d3h4x0r commented 4 years ago

I'm unable to find any git repo containing the raw files present in the Linux filesystem, so I don't see a way to prepare a pull request myself.

So, here are my versions of the two files I modified to get all of this working nicely:

/usr/sbin/btpair:

#!/bin/bash

echo Switch input device
echo to pairing mode.
echo searching...
echo
IFS=$'\n'; MACS=( $(hcitool scan --flush | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}[[:space:]]*.*') )
if [ ! -z "${MACS[*]}" ]; then
        for MAC in "${MACS[@]}"
        do
                NAME="$(echo "${MAC##*$'\t'}" )"
                MAC="$(echo "${MAC%$'\t'*}" )"
                echo Found device: $MAC \($NAME\)
                echo Pairing...
                if pair-agent hci0 $MAC; then
                        echo Add to trust list...
                        MAC=$(echo $MAC | sed -e 's/:/_/g')
                        BTADAPTER=`dbus-send --system --dest=org.bluez --print-reply / org.bluez.Manager.DefaultAdapter | tail -1 | sed 's/^.*"\(.*\)".*$/\1/'`
                        dbus-send --system --dest=org.bluez --print-reply $BTADAPTER/dev_$MAC org.bluez.Device.SetProperty string:Trusted variant:boolean:true >/dev/nul
                        echo Connecting...
                        dbus-send --system --dest=org.bluez --print-reply $BTADAPTER/dev_$MAC org.bluez.Input.Connect >/dev/nul
                        echo Done.
                else
                        echo Skipped.
                fi
                echo
        done
else
        echo nothing found.
fi

/usr/sbin/pair-agent:

#!/usr/bin/python

from __future__ import absolute_import, print_function, unicode_literals

import gobject as GObject

import sys
import dbus
import dbus.service
import dbus.mainloop.glib
from optparse import OptionParser
from dbus import DBusException

def ask(prompt):
        try:
                return raw_input(prompt)
        except:
                return input(prompt)

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

class Agent(dbus.service.Object):
        _singleton = None

        def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                assert Agent._singleton is None
                Agent._singleton = self
                self.exit_on_release = True
                self._mainloop = GObject.MainLoop()
                self._result = 0

        def run(self):
                self._mainloop.run()
                return self._result

        def quit(self, result=0):
                self._mainloop.quit()
                self._result = result

        def set_exit_on_release(self, exit_on_release):
                self.exit_on_release = exit_on_release

        @dbus.service.method("org.bluez.Agent",
                                        in_signature="", out_signature="")
        def Release(self):
                if self.exit_on_release:
                        self.quit()

        @dbus.service.method("org.bluez.Agent",
                                        in_signature="os", out_signature="")
        def Authorize(self, device, uuid):
                print("Authorize (%s, %s)" % (device, uuid))
                authorize = ask("Authorize connection (yes/no): ")
                if (authorize == "yes"):
                        return
                raise Rejected("Connection rejected by user")

        @dbus.service.method("org.bluez.Agent",
                                        in_signature="o", out_signature="s")
        def RequestPinCode(self, device):
                print("Enter PIN 0000 on device\n")
                sys.stdout.flush()
                return "0000"

        @dbus.service.method("org.bluez.Agent",
                                        in_signature="o", out_signature="u")
        def RequestPasskey(self, device):
                print("Enter PIN 1234 on device\n")
                sys.stdout.flush()
                return dbus.UInt32(1234)

        @dbus.service.method("org.bluez.Agent",
                                        in_signature="ou", out_signature="")
        def DisplayPasskey(self, device, passkey):
                print("DisplayPasskey (%s, %06d)" % (device, passkey))

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

        @dbus.service.method("org.bluez.Agent",
                                        in_signature="ou", out_signature="")
        def RequestConfirmation(self, device, passkey):
                print("RequestConfirmation (%s, %06d)" % (device, passkey))
                confirm = ask("Confirm passkey (yes/no): ")
                if (confirm == "yes"):
                        return
                raise Rejected("Passkey doesn't match")

        @dbus.service.method("org.bluez.Agent",
                                        in_signature="s", out_signature="")
        def ConfirmModeChange(self, mode):
                print("ConfirmModeChange (%s)" % (mode))
                authorize = ask("Authorize mode change (yes/no): ")
                if (authorize == "yes"):
                        return
                raise Rejected("Mode change by user")

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

        @classmethod
        def create_device_reply(cls, device):
                print("New device (%s)" % (device))
                cls._singleton.quit()

        @classmethod
        def create_device_error(cls, error):
                print("Creating device failed: %s" % (error))
                cls._singleton.quit(1)

if __name__ == '__main__':
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

        bus = dbus.SystemBus()
        manager = dbus.Interface(bus.get_object("org.bluez", "/"),
                                                        "org.bluez.Manager")

        capability = "KeyboardDisplay"

        parser = OptionParser()
        parser.add_option("-c", "--capability", action="store",
                                        type="string", dest="capability")
        (options, args) = parser.parse_args()
        if options.capability:
                capability  = options.capability

        if len(args) > 0:
                path = manager.FindAdapter(args[0])
        else:
                path = manager.DefaultAdapter()

        adapter = dbus.Interface(bus.get_object("org.bluez", path),
                                                        "org.bluez.Adapter")

        path = "/test/agent"
        agent = Agent(bus, path)

        if len(args) > 1:
                try:
                        device = adapter.FindDevice(args[1])
                        adapter.RemoveDevice(device)
                except DBusException:
                        pass

                agent.set_exit_on_release(False)
                adapter.CreatePairedDevice(args[1], path, capability,
                                        timeout=60000,
                                        reply_handler=agent.create_device_reply,
                                        error_handler=agent.create_device_error)
        else:
                adapter.RegisterAgent(path, capability)
                print("Agent registered")

        result = agent.run()

        #adapter.UnregisterAgent(path)
        #print("Agent unregistered")

        sys.exit(result)
sorgelig commented 4 years ago

Thanks! i will import your changes into Linux

c0d3h4x0r commented 4 years ago

One more small change (for better readability when running the script inside the narrow OSD):

Change this line:

echo Found device: $MAC \($NAME\)

to this:

echo Found device:
echo $NAME
echo $MAC
c0d3h4x0r commented 4 years ago

I’m still working on additional functional improvements to this... I should have another revision soon.

c0d3h4x0r commented 4 years ago

Yep, current revision I’m working on finally fixes the inconsistent device discovery behavior. Root cause was pair-agent using the same path for both the adapter and the agent.

Now I’m just tying to figure out how to get CreatePairedDevice() to actually honor the capability option. (Note: KeyboardDisplay isn’t even a valid capability choice according to the bluez API docs — presumably DisplayYesNo was intended. But there’s also no stdin stream connected between the OSD and btpair, so it ought to be passing DisplayOnly instead.) I’m close to having this last piece solved.

sorgelig commented 4 years ago

for keyboard pairing there are usually 2 options: 1) some internals from BT stack offer a key, and it's displayed like "type 1234 on BT keyboard and press enter" 2) use hardcoded in script value (can be 0000) and then use the same as (1)

c0d3h4x0r commented 4 years ago

Right. Just trying to make sure that keyboard, gamepad, and other device types all pair in a reasonable way via the OSD before I call this ready to go.

c0d3h4x0r commented 4 years ago

The multi-gamepad case is interesting, because as-is, the script forcibly removes and repairs every already-paired device, which actually causes my already-paired gamepad to power itself off (in response to being forcibly removed), such that it then fails to re-pair. So I’m revising that logic to check if the device is already paired and if so, don’t do anything.

c0d3h4x0r commented 4 years ago

Looks like part of what I’ve been struggling with is maybe a buggy bluetooth dongle or driver... it sometimes gets stuck in a bad state where it thinks it’s scanning but hcitool scan --flush doesn’t find anything. Looks like a call to hciconfig hci0 reset resolves it, so I’m going to add a forcible call to it immediately before.

c0d3h4x0r commented 4 years ago

Alright, I've finally got this whipped into what I'd call "ship ready".

Summary:

The final work consists of changes to one new file and three existing files:

/usr/sbin/btscan (new):

#!/bin/bash

IFS=$'\n'
MAC_NAMES_ERE='([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}.*'
MAC_NAMES=( $( hcitool scan --flush | grep -o -E "$MAC_NAMES_ERE" ) )

# TODO: Investigate more this issue of Bluetooth adapters getting stuck in
# a bad state.  As far as I can tell, this may be a result of bugs in D-bus,
# bluez, dbus-python, hcitool, or maybe in more than one of them.  It does
# not appear to be a hardware-specific bug because I was able to repro
# the same issue using three different Bluetooth dongles using different
# chipsets.
#
# Ensure already-paired devices can auto-reconnect when powered on.
#hciconfig hci0 pscan
#
# Some BT adapters get stuck in a bad state wherein they
# claim to be page scanning but cannot find (or be discovered by)
# any remote Bluetooth devices.  Work around this case by resetting
# the dongle and trying again.
#if [[ -z "${MAC_NAMES[*]}" ]] && hciconfig hci0 | grep -qoF "UP RUNNING PSCAN"; then
#   hciconfig hci0 reset
#   hciconfig hci0 noscan
#   MAC_NAMES=( $( hcitool scan --flush | grep -o -E "$MAC_NAMES_ERE" ) )
#fi

for MAC_NAME in "${MAC_NAMES[@]}"; do
    echo $MAC_NAME
done

/usr/sbin/btpair (modified):

#!/bin/bash

trap -- '' SIGINT

echo Switch input devices
echo to pairing mode.
echo Searching...
echo

OLD_IFS="$IFS"
IFS=$'\n'
MAC_NAMES=( $( btscan ) )
IFS="$OLD_IFS"

if [ -z "${MAC_NAMES[*]}" ]; then
    echo nothing found.
    exit 0
fi

for MAC_NAME in "${MAC_NAMES[@]}"; do 
    MAC=$(echo "${MAC_NAME}" | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}')
    NAME=$(echo "${MAC_NAME}" | sed 's/[^   ]*[     ]*//')
    echo Found: $NAME
    echo MAC: $MAC
    pair-agent --capability NoInputNoOutput hci0 $MAC
    echo
done

/usr/sbin/pair-agent (modified):

#!/usr/bin/python

from __future__ import absolute_import, print_function, unicode_literals

import dbus
from dbus import DBusException
import dbus.service
import dbus.mainloop.glib
import gobject as GObject
from optparse import OptionParser
import signal
import sys

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

def get_bluez_object(path_, type_):
    assert path_
    obj = _SYSTEM_BUS.get_object("org.bluez", path_)
    return dbus.Interface(obj, "org.bluez.%s" % (type_))

class Agent(dbus.service.Object):
    _singleton = None

    class Rejected(dbus.DBusException):
        def get_dbus_name():
            return "org.bluez.Agent.Rejected"

    def __init__(self, adapter, adapter_path, *args, **kwargs):
        assert Agent._singleton is None
        Agent._singleton = self
        self._path = adapter_path + "/agent"
        super().__init__(conn=_SYSTEM_BUS, object_path=self._path)
        self._adapter = adapter
        self._adapter_path = adapter_path
        self._mainloop = GObject.MainLoop()
        self._error = None

    @staticmethod
    def _ask(prompt):
        try:
            return raw_input(prompt)
        except:
            return input(prompt)

    @dbus.service.method("org.bluez.Agent",
                    in_signature="os", out_signature="")
    def Authorize(self, device, uuid):
        authorize = self._ask("Authorize connection (yes/no): ")
        if (authorize != "yes"):
            raise Rejected("Connection rejected by user.")

    @dbus.service.method("org.bluez.Agent",
                    in_signature="o", out_signature="s")
    def RequestPinCode(self, device):
        pin = self._ask("Enter device PIN\nor <ENTER> to skip: ")
        return pin

    @dbus.service.method("org.bluez.Agent",
                    in_signature="o", out_signature="u")
    def RequestPasskey(self, device):
        passkey = self._ask("Enter device passkey\nor <ENTER> to skip: ")
        return dbus.UInt32(passkey)

    @dbus.service.method("org.bluez.Agent",
                    in_signature="ou", out_signature="")
    def DisplayPasskey(self, device, passkey):
        print("Enter this passkey\non your device: %06d" % (passkey))
        sys.stdout.flush()

    @dbus.service.method("org.bluez.Agent",
                    in_signature="os", out_signature="")
    def DisplayPinCode(self, device, pincode):
        print("Enter this PIN\non your device: %s" % (pincode))
        sys.stdout.flush()

    @dbus.service.method("org.bluez.Agent",
                    in_signature="ou", out_signature="")
    def RequestConfirmation(self, device, passkey):
        confirm = self._ask("Is %06d the\ndevice passkey?\nType 'yes' to confirm\nor <ENTER> to skip: " % (passkey))
        if (confirm != "yes"):
            raise Rejected("Passkey rejected by user.")

    @dbus.service.method("org.bluez.Agent",
                    in_signature="s", out_signature="")
    def ConfirmModeChange(self, mode):
        print("Device is requesting change\nto mode: %s" % (mode))
        authorize = self._ask("Type 'yes' to confirm\nor <ENTER> to skip: ")
        if (authorize != "yes"):
            raise Rejected("Mode change rejected by user.")

    @classmethod
    def create_paired_device_reply(cls, device):
        cls._singleton._mainloop.quit()

    @classmethod
    def create_paired_device_error(cls, error):
        if cls.Rejected.get_dbus_name() == error.get_dbus_name():
            print(error.get_dbus_message())
        else:
            print("ERROR: %s" % (error.get_dbus_message()))
        cls._singleton._error = error
        cls._singleton._mainloop.quit()

    def create_paired_device(self, mac, capability):
        self._adapter.CreatePairedDevice(
            mac, self._path, capability, timeout=10000,
            reply_handler=Agent.create_paired_device_reply,
            error_handler=Agent.create_paired_device_error)
        self._mainloop.run()
        if self._error is not None:
            sys.exit(1)
        return self._adapter.FindDevice(mac)

def create_paired_device(adapter, adapter_path, mac, capability):
    agent = Agent(adapter, adapter_path)    
    return agent.create_paired_device(mac, capability)

def interrupt_handler(signum, frame):
    # Prevent user from interrupting this script using
    # CTRL+C or CTRL+BREAK!  Be like Weird Al Yankovich
    # (i.e. just eat it).
    pass

if __name__ == '__main__':
    signal.signal(signal.SIGINT, interrupt_handler)

    manager = get_bluez_object("/", "Manager")

    parser = OptionParser()
    parser.add_option("-c", "--capability", action="store",
                type="string", dest="capability",
                default="DisplayYesNo")
    (options, args) = parser.parse_args()
    assert len(args) == 2
    adapter_name = args[0]
    device_mac = args[1]

    print("Pairing...")
    sys.stdout.flush()

    adapter_path = manager.FindAdapter(adapter_name)
    adapter = get_bluez_object(adapter_path, "Adapter")

    try:
        device_path = adapter.FindDevice(device_mac)
        adapter.RemoveDevice(device_path)
    except DBusException:
        pass

    device_path = create_paired_device(
                adapter, adapter_path,
                device_mac, options.capability)

    print("Trusting...")
    sys.stdout.flush()
    device = get_bluez_object(device_path, "Device")
    device.SetProperty("Trusted", True)

    try:
        print("Connecting...")
        sys.stdout.flush()
        input_device = get_bluez_object(device_path, "Input")
        input_device.Connect()
        print("Done.")
        sys.stdout.flush()
    except DBusException as e:
        if e.get_dbus_name() == "org.bluez.Error.ConnectionAttemptFailed":
            print("failed.")
        else:
            print(str(e))
        sys.exit(1)

/media/fat/Scripts/bluetooth_pairing.sh (modified):

#!/bin/bash

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# 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 <http://www.gnu.org/licenses/>.

# Original script by Alexey "Sorgelig" Melnikov https://github.com/MiSTer-devel/Main_MiSTer/wiki
# Copyright 2019 Alessandro "Locutus73" Miele

# You can download the latest version of this script from:
# https://github.com/MiSTer-devel/Scripts_MiSTer

# Version 1.0 - 2019-10-26 - First commit.

# ========= OPTIONS ==================

# ========= ADVANCED OPTIONS =========

DIALOG_HEIGHT="31"

# ========= CODE STARTS HERE =========

function checkTERMINAL {
#   if [ "$(uname -n)" != "MiSTer" ]
#   then
#       echo "This script must be run"
#       echo "on a MiSTer system."
#       exit 1
#   fi
    if [[ ! (-t 0 && -t 1 && -t 2) ]]
    then
        echo "This script must be run"
        echo "from an interactive terminal."
        echo "Please press F9 (F12 to exit)"
        echo "or use SSH."
        exit 2
    fi
}

function setupDIALOG {
    DIALOG="dialog"

    export NCURSES_NO_UTF8_ACS=1

    : ${DIALOG_OK=0}
    : ${DIALOG_CANCEL=1}
    : ${DIALOG_HELP=2}
    : ${DIALOG_EXTRA=3}
    : ${DIALOG_ITEM_HELP=4}
    : ${DIALOG_ESC=255}

    : ${SIG_NONE=0}
    : ${SIG_HUP=1}
    : ${SIG_INT=2}
    : ${SIG_QUIT=3}
    : ${SIG_KILL=9}
    : ${SIG_TERM=15}
}

function setupDIALOGtempfile {
    DIALOG_TEMPFILE=`(DIALOG_TEMPFILE) 2>/dev/null` || DIALOG_TEMPFILE=/tmp/dialog_tempfile$$
    trap "rm -f $DIALOG_TEMPFILE" 0 $SIG_NONE $SIG_HUP $SIG_INT $SIG_QUIT $SIG_TERM
}

function readDIALOGtempfile {
    DIALOG_RETVAL=$?
    DIALOG_OUTPUT="$(cat ${DIALOG_TEMPFILE})"
    #rm -f ${DIALOG_TEMPFILE}
    #unset DIALOG_TEMPFILE
}

checkTERMINAL

RESCAN=true

while $RESCAN; do
    RESCAN=false

    clear
    echo Switch input devices
    echo to pairing mode.
    echo Searching...
    echo

    OLD_IFS="$IFS"
    IFS=$'\n'
    MAC_NAMES=( $( btscan ) )
    IFS="$OLD_IFS"

    unset MENU_ITEMS
    if [ ! -z "${MAC_NAMES[*]}" ]; then
        for MAC_NAME in "${MAC_NAMES[@]}"; do
            MAC=$(echo "${MAC_NAME}" | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}')
            NAME=$(echo "${MAC_NAME}" | sed 's/[^   ]*[     ]*//')
            MENU_ITEMS="${MENU_ITEMS} \"${MAC}\" \"${NAME}\""
        done

        setupDIALOG
        setupDIALOGtempfile
        eval ${DIALOG} --clear --colors --ok-label \"Pair\" \
            --title \"Bluetooth pair\" \
            --extra-button --extra-label \"Rescan\"\
            --menu \"Please select the Controller/Keyboard/Mouse you want to pair.\" ${DIALOG_HEIGHT} 0 999 \
            ${MENU_ITEMS} \
            2> ${DIALOG_TEMPFILE}
        readDIALOGtempfile

        case ${DIALOG_RETVAL} in
            ${DIALOG_OK})
                clear
                MAC="${DIALOG_OUTPUT}"
                ;;
            ${DIALOG_EXTRA})
                RESCAN=true
                ;;
            ${DIALOG_CANCEL})
                clear
                exit 0
                ;;
            *)
                exit 1
                ;;
        esac
    fi
done

if [ ! -z "${MAC}" ]; then
    pair-agent hci0 $MAC
else
    echo nothing found.
fi
c0d3h4x0r commented 4 years ago

Ideas for future improvement:

sorgelig commented 4 years ago

Thanks for rework! Bluez 5x has very complicated pairing. It uses ugly console tool in interactive mode to pair. So that's definitely not a good for MiSTer. Bluez 4 (current) is more friendly for scripts. I will include your files into linux except bluetooth_pairing.sh which you need to PR to scripts repository.

c0d3h4x0r commented 4 years ago

Great, thanks. I’ll create the PR asap.

bluez 5 doesn’t support D-Bus anymore?

sorgelig commented 4 years ago

I don't remember details already, but d-bus commands are very different in Bluez 5

c0d3h4x0r commented 4 years ago

They can't be too different, because Bluetooth itself hasn't fundamentally changed. The API surface may be different, but it has to accomplish all the same goals and concepts, so I'd think it should be pretty trivial to port this code forward.

I do know that I had to manually update my RetroPie distro to a newer version of bluez (which I had to build from source, as no precompiled binaries were made available for it) in order to resolve some Bluetooth stability problems I'd been having with it. So I'm very suspicious of bluez being the cause of the adapter getting stuck in a bad state.

sorgelig commented 4 years ago

believe me, the difference between 4 and 5 are fundamental. That's why buildroot even has separate option to use either 4.x or 5.x. If they would be compatible, then they wouldn't have such option. When you update some distro it already has everything updated to accommodate the differences.

At the end, it's Linux which means anarchy of APIs. Touch some package and you will find yourself at the end of day updating the whole linux :)

c0d3h4x0r commented 4 years ago

Yep, agreed -- Linux dependency management has always made Windows' "DLL Hell" look like a walk in the park :-)

c0d3h4x0r commented 4 years ago

I finally figured out the cause of another related Bluetooth reliability problem! Sometimes the Bluetooth adapter would not be initialized on startup, and it appears to be caused by the RememberPowered = true setting in /etc/bluetooth/main.conf. That behavior appears to set up a race condition with the hciconfig hci0 up code in /etc/init.d/S35bluetooth.

I changed that line to RememberPowered = false, and now the Bluetooth adapter always starts right up (way faster than before) on every boot.

This issue was exposed by the /media/fat/Scripts/shutdown.sh script I added (after discovering that just killing the power at the OSD actually corrupts the exFAT) for clean software shutdown from the OSD. Apparently the shutdown sequence in /etc/inittab results in powering off the Bluetooth adapter, such that on next boot, the bluetooth service tries to restore the remembered “off” state while the startup sequence is simultaneously trying to turn the adapter on.

So I’d recommend changing that line in /etc/bluetooth/main.conf.

I’ll file a new issue on the exFAT corruption shutdown problem and create a PR for the little shutdown script I wrote.

sorgelig commented 4 years ago

Thanks for investigating. I will modify /etc/bluetooth/main.conf

sorgelig commented 4 years ago

May be InitiallyPowered should be also set to false, so in case if some dongles aren't powered on restart, they will be explicitly powered by system.

sorgelig commented 4 years ago

InitiallyPowered = false make BT dongle not powered at boot.

c0d3h4x0r commented 4 years ago

I think it’s bad to have S35bluetooth and main.conf both trying to control the adapter’s power state at the same time. Do main.conf and InitiallyPowered always get applied on OS startup, not just cold boot from power off? If so, maybe we should just remove the “hciconfig up” calls from S35bluetooth and rely on InitiallyPowered=true to do the work for each boot. I can try it and report back with my findings.

c0d3h4x0r commented 4 years ago

But that can set up another race condition between InitiallyPowered=true and the rest of the work in S35bluetooth, so I think the “hciconfig up” calls are necessary for the rest of S35bluetooth to work reliably. So probably best to leave InitiallyPowered=true, so at least it agrees with what S35bluetooth is trying to do anyway. Even though it’s not great to have them both trying to do the same thing at the same time, at least that way it doesn’t matter who wins the race.

c0d3h4x0r commented 4 years ago

Is there anything more you want me to try while all of this is still fresh in my brain, or do you consider my work here complete?

ChloeMarieTaylor76 commented 4 years ago

@c0d3h4x0r Thank you so much, I tried your most recent changes and it fixed the issues I was having. I have two tvs with bluetooth as well as two nvidia shield tvs. I only ever managed to get things working with sheer persistence of numerous attempts and turning off the two tvs and the shields.

I tried your scripts and it connected my keyboard, a m30 pad and a sn30 pad together with no issues, it even tried to connected to my tv but I selected deny on my tv screen and it continued with an unauthorised error warning but no ill effects.

I am using a Broadcom BCM20702 usb bluetooth adapter that I had to add firmware for to the lib/firmware folder.

sorgelig commented 4 years ago

Since i have no problem with pairing originally i can't say if anything else need to be done. :)

c0d3h4x0r commented 4 years ago

Happy to help!

sorgelig commented 4 years ago

I plan to release Linux update soon, so will include your changes too.

c0d3h4x0r commented 4 years ago

You can go ahead and close this one now. :)