After few days fiddling with BlueZ 5 this is what I've got.
Using BlueZ 5.50 & Raspbian Stretch (Pi Zero W):
Start bluetoothd with --compat:
append to ExecStart line in /etc/systemd/system/dbus-org.bluez.service
or
in rc.local: sudo bluetoothd --compat &
The next steps are handled by code posted below but to clarify, hciconfig needs to be set to:
sudo hciconfig hci0 sspmode 0
Note #1: With 'sspmode 1' when pairing from Android you will get a prompt for PIN but afterwards Pi autogenerates 6-digit passkey and pairing failes.
Note #2: hciconfig hci0 can't be set with auth or encrypt those will actually register agent DisplayOnly (we will be creating agent in next step) as KeyboardDisplay (sudo btmon to verify) and pairing won't use predefined PIN. Not sure if there is a reason why DisplayOnly can't use auth, encrypt (might have something to do with them setting security mode 3).
Afterwards we will use bluetoothctl:
pi@raspberrypi:~ $ bluetoothctl
Agent registered
[bluetooth]# agent off
Agent unregistered
[bluetooth]# agent DisplayOnly
Agent registered
[bluetooth]# default-agent
Default agent request successful
[bluetooth]# discoverable on
Changing discoverable on succeeded
[CHG] Controller 11:22:33:44:55:66 Discoverable: yes
[bluetooth]# pairable on
Changing pairable on succeeded
[CHG] Controller 11:22:33:44:55:66 Pairable: yes
// Initiate pairing on remote device //
[NEW] Device AA:BB:CC:DD:EE:FF Android_phone
// Enter any PIN on Device AA:BB:CC:DD:EE:FF
Request PIN code
// retype your PIN below (on Pi)
[agent] Enter PIN code: <your PIN>
[CHG] Device AA:BB:CC:DD:EE:FF Class: 0x005a020c
...
[CHG] Device AA:BB:CC:DD:EE:FF Paired: yes
[bluetooth]# quit
Note #3: Registering agent using pexpect (just a note if you try to run the code posted below) with BlueZ 5.43 (default version in Stretch) is hit and miss
Below is a Python 2.7 code that sets sspmode and handles pairing with pregenerated PIN:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function # import print from python3: end=""
import time
import re
import pexpect # sudo apt-get install python-pexpect
import subprocess
import random
# !!! make sure bluetoothd runs in --compat mode before executing this script !!!
def pair_with_pin(start_time, pin, time_limit=60): # int(time.time()), pin - \d{4}, time_limit - approximate pairing window time in seconds, it might take up to 2x (nested timeout conditions)
"exectutes pairing with entered PIN on bluetooth adapter side"
pairing_status = False
try:
subprocess.call(['sudo','hciconfig','hci0','sspmode', '0'])
# bluetoothctl
child = pexpect.spawn('bluetoothctl')
child.expect("#")
child.sendline('agent off') # might be unnecessary
child.expect("unregistered")
child.sendline('agent DisplayOnly')
child.expect("Agent registered")
child.sendline('pairable on')
child.expect("pairable on succeeded")
child.sendline('discoverable on')
child.expect("discoverable on succeeded")
child.sendline('default-agent')
print ('Please input PIN: ' + pin)
# waiting for Phone to send a pairing request...
child.expect('Enter PIN code:', timeout = time_limit ) # timeout <= PAIRING_TIME_LIMIT to keep some kind of logic
while int(time.time()) < start_time + time_limit: # allow multiple pairing attempts during pairing window
child.sendline(pin)
i = child.expect(['Paired: yes', 'Enter PIN code:'], timeout = time_limit)
if i == 0: # found 'Paired: yes' == successful pairing
trust_mac = 'trust ' + re.search(r'(?:[0-9a-fA-F]:?){12}.+$', child.before).group(0) # extract MAC from last line, one with 'Paired: Yes'
child.sendline(trust_mac) # optionally add device to trusted
child.expect('trust succeeded', timeout = 10)
pairing_status = True
break
#else: # i == 1
# print('wrong PIN, retrying if time will allow')
except pexpect.EOF:
print ('!!!!!!!! EOF')
except pexpect.TIMEOUT:
print ('!!!!!!!! TIMEOUT')
# hide Pi's bluetooth for security reasons
child.sendline('pairable off')
child.expect("pairable off succeeded")
child.sendline('discoverable off')
child.expect("discoverable off succeeded")
child.close()
return pairing_status
#main program body
PAIRING_TIME_LIMIT = 60
BT_PIN = random.randint(1000,10000) # generate random 4-digit PIN 1000..9999
status = pair_with_pin(int(time.time()), str(BT_PIN), PAIRING_TIME_LIMIT)
if status == True:
print('Pairing successful')
Final note: After successful pairing it might be posibble to turn encryption on, try:
hciconfig hci0 encrypt
or
hcitool enc $BDADD