Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esp32: 'BLE' object has no attribute 'gap_pair' #7008

Closed
z7jzk opened this issue Mar 10, 2021 · 20 comments
Closed

esp32: 'BLE' object has no attribute 'gap_pair' #7008

z7jzk opened this issue Mar 10, 2021 · 20 comments

Comments

@z7jzk
Copy link

z7jzk commented Mar 10, 2021

Generic ESP32 implementation. Tried on both v1.14 esp32-idf4-20210202-v1.14.bin and v1.14-unstable latest esp32-20210310-unstable-v1.14-84-g59a129f22.bin.

self._ble.gap_pair(self._conn_handle)

yields error:

AttributeError: 'BLE' object has no attribute 'gap_pair'

Is gap_pair(conn_handle,/) ready to use yet? I'm having issues in re-connecting to my bluetooth device as I need to pair it to add to trusted devices (connecting alone doesn't seem adequate).

ubluetooth — low-level Bluetooth: gap_pair

@dpgeorge
Copy link
Member

Pairing/bonding is not yet supported on the esp32, because the esp32 needs a bit of work to switch it to synchronous BLE events.

@z7jzk
Copy link
Author

z7jzk commented Mar 12, 2021

Thanks @dpgeorge! Any idea when it will be accessible on esp32? I'll see if an STM32 will work in the near term for our project.

@dpgeorge
Copy link
Member

Any idea when it will be accessible on esp32?

No, sorry. There are a lot of other things competing for development time!

@z7jzk
Copy link
Author

z7jzk commented Mar 15, 2021

Understood. I was wondering directionally when this may be ready as it will affect the hardware we end up going with for our project.

@dpgeorge
Copy link
Member

I had a quick look at this the other day and made some progress, but it needs more time. I might be able to make a patch for you to try out.

@z7jzk
Copy link
Author

z7jzk commented Mar 15, 2021

That would be great! Keep me posted and I'll give it a shot.

@dpgeorge
Copy link
Member

Please see #7046 for work-in-progress on this feature. @z7jzk any feedback on that would be great!

@z7jzk
Copy link
Author

z7jzk commented Mar 17, 2021

I'll pull and start testing. I'll keep you posted on how it goes! Thank you for the quick response on this one!

@z7jzk
Copy link
Author

z7jzk commented Mar 18, 2021

@dpgeorge, that did it! I was able to successfully pair to my peripheral bt device and the 2 devices connect on restart successfully each time, so it seems they are both added in the trusted devices. Need me to review your PR?

@dpgeorge
Copy link
Member

and the 2 devices connect on restart successfully each time, so it seems they are both added in the trusted devices.

Did you implement the BLE secret IRQs to get this working?

@dpgeorge
Copy link
Member

Need me to review your PR?

Not at this stage, thanks. It's still a WIP.

@z7jzk
Copy link
Author

z7jzk commented Mar 19, 2021

Did you implement the BLE secret IRQs to get this working?

I did not, but it seems that the pairing functionality allows my peripheral device to store the esp32 I'm using as a central in the trusted devices, so perhaps that is why this is working now!

@z7jzk
Copy link
Author

z7jzk commented Apr 7, 2021

Did you implement the BLE secret IRQs to get this working?

@dpgeorge, I implemented the _IRQ_GET_SECRET and _IRQ_SET_SECRET events tonight to store the bonding secrets more permanently, so that I don't have to put my peripheral into pairing mode each time I connect (the device is able to connect without going into pairing mode, but the devices are not able to communicate with one another).

I get the following error before the device reboots (this happens continuously):

Guru Meditation Error: Core  0 panic'ed (StoreProhibited). Exception was unhandled.
Core 0 register dump:
PC      : 0x400da29d  PS      : 0x00060430  A0      : 0x800dc83b  A1      : 0x3ffcfd80
A2      : 0x3ffeb4a0  A3      : 0x3ffeb4a0  A4      : 0x00000000  A5      : 0x00000003
A6      : 0x3ffeb7cc  A7      : 0x00000064  A8      : 0x00000001  A9      : 0x3ffcfd60
A10     : 0x3ffcfee0  A11     : 0x3ffbd82c  A12     : 0x000000d7  A13     : 0x000004ea
A14     : 0x3ffcfd28  A15     : 0x00000002  SAR     : 0x00000004  EXCCAUSE: 0x0000001d
EXCVADDR: 0x00000005  LBEG    : 0x4000c46c  LEND    : 0x4000c477  LCOUNT  : 0x00000000

I see you have additional commits on #7046 as I write this, so I'll pull and run against the latest updates and report back!

@z7jzk
Copy link
Author

z7jzk commented Apr 7, 2021

When I implement _IRQ_GET_SECRET per the documentation at Micropython uBluetooth Event-Handling, I get an error

NameError: local variable referenced before assignment

elif event == _IRQ_GET_SECRET:
        # Return a stored secret.
        # If key is None, return the index'th value of this sec_type.
        # Otherwise return the corresponding value for this sec_type and key.
        sec_type, index, key = data
        return value         <~ Error is happening at this line

value is not defined per above, so I was wondering what exactly I should return?

@dpgeorge
Copy link
Member

value is not defined per above, so I was wondering what exactly I should return?

You may want to refer to examples/bluetooth/ble_bonding_peripheral.py.

@z7jzk
Copy link
Author

z7jzk commented Apr 13, 2021

@dpgeorge thanks! I have been working through that example. I'm doing a little refactoring as the secrets are not being stored in the json file (cleaning this up). I'll have to follow up later tonight with where I'm at.

@z7jzk
Copy link
Author

z7jzk commented Apr 24, 2021

@dpgeorge, I'm sorry to be a continued pain, but I'm still struggling a bit to fully implement pairing on my device. I've referenced the example at examples/bluetooth/ble_bonding_peripheral.py and have a few questions yet. To add more context of the device I'm trying to setup, I'm planning to use my ESP32 as a Central device that will connect to a Peripheral device that requires a bonded connection.

  1. When does the IRQ event _IRQ_SET_SECRET get called? Will _IRQ_SET_SECRET be called with the configuration I'm going after?
  2. I've setup my code to use a hybrid from the ble_bonding_peripheral.py and ble_simple_central.py files; when _save_secrets() is called, the variables sec_type, value, and key are saved in the secrets.json file; however, I'm not sure when the appropriate time to set these variables are.
def _save_secrets(self):
        try:
            with open(self.secFile, 'w') as f:
                json_secrets = [
                    (sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value))
                    for (sec_type, key), value in self._secrets.items()
                ]
                json.dump(json_secrets, f)
        except:
            print('failed to save secrets')

Here is a full copy of the file I'm using for reference:

file I'm calling the class in bluetooth file from:

ble = bluetooth.BLE()
addrType=1
self.btc = btu.BLECentral(ble, self.bt_addr, self.bt_serv_uuid, self.bt_char_uuid)
self.btc.connect(addr_type=addrType, addr=self.bt_addr)

bluetooth file:

import bluetooth
import struct
import json
import binascii
from ble_advertising import advertising_payload
from ble_advertising import decode_services, decode_name
from micropython import const

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_SCAN_RESULT = const(5)
_IRQ_SCAN_DONE = const(6)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_SERVICE_RESULT = const(9)
_IRQ_GATTC_SERVICE_DONE = const(10)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_GATTC_READ_DONE = const(16)
_IRQ_GATTC_WRITE_DONE = const(17)
_IRQ_GATTC_NOTIFY = const(18)
_IRQ_GATTC_INDICATE = const(19)
_IRQ_GATTS_INDICATE_DONE = const(20)
_IRQ_ENCRYPTION_UPDATE = const(28)
_IRQ_MTU_EXCHANGED = const(21)
_IRQ_GET_SECRET = const(29)
_IRQ_SET_SECRET = const(30)
_IRQ_PASSKEY_ACTION = const(31)

_ADV_IND = const(0x00)
_ADV_DIRECT_IND = const(0x01)
_ADV_SCAN_IND = const(0x02)
_ADV_NONCONN_IND = const(0x03)

_PASSKEY_ACTION_INPUT = const(2)
_PASSKEY_ACTION_DISP = const(3)
_PASSKEY_ACTION_NUMCMP = const(4)

_FLAG_READ_ENCRYPTED = const(0x0200)
_IO_CAPABILITY_DISPLAY_YESNO = const(1)

_UART_SERVICE_UUID = bluetooth.UUID(0xfea6)
_UART_RX_CHAR_UUID = bluetooth.UUID('b5f90072-aa8d-11e3-9046-0002a5d5c51b')
_UART_TX_CHAR_UUID = bluetooth.UUID('b5f90073-aa8d-11e3-9046-0002a5d5c51b')

class BLECentral:
    def __init__(self, ble, target_mac='', target_s_uuid='', target_c_uuid=''):
        self._reset()
        self._ble = ble
        self.secFile = './secrets.json'
        self._load_secrets()
        self._ble.irq(self._irq)
        self._ble.config(bond=True)
        self._ble.config(le_secure=True)
        self._ble.config(mitm=True)
        # self._ble.config(io=_IO_CAPABILITY_DISPLAY_YESNO)
        # self._ble.active(True)
        # self._ble.config(addr_mode=2)
        self.target_mac = target_mac.strip().replace(':','').lower().encode('utf_8')
        self.target_s_uuid = target_s_uuid
        self.target_c_uuid = target_c_uuid
        if not target_s_uuid == '':
            _UART_SERVICE_UUID = bluetooth.UUID(target_s_uuid)
            print(_UART_SERVICE_UUID)
        if not target_c_uuid == '':
            print('_UART_TX_CHAR_UUID: %s' % (_UART_TX_CHAR_UUID))
            _UART_RX_CHAR_UUID = bluetooth.UUID(target_c_uuid)
            print('_UART_RX_CHAR_UUID: %s' % (_UART_RX_CHAR_UUID))

    def _reset(self):
        # Cached name and address from a successful scan.
        self._name = None
        self._addr_type = None
        self._addr = None

        # Callbacks for completion of various operations.
        # These reset back to None after being invoked.
        self._scan_callback = None
        self._conn_callback = None
        self._read_callback = None

        # Persistent callback for when new data is notified from the device.
        self._notify_callback = None

        # Connected device.
        self._conn_handle = None
        self._start_handle = None
        self._end_handle = None
        self._tx_handle = None
        self._rx_handle = None

        self._sec_type = None
        self._key = None
        self._value = None

    def _irq(self, event, data):
        if event == _IRQ_PERIPHERAL_CONNECT:
            conn_handle, addr_type, addr = data
            print('_IRQ_PERIPHERAL_CONNECT:', conn_handle, addr_type, addr)
            self._conn_handle = conn_handle
            self._ble.gattc_discover_services(self._conn_handle)
        elif event == _IRQ_PERIPHERAL_DISCONNECT:
            print('_IRQ_PERIPHERAL_DISCONNECT')
            conn_handle, _, _ = data
            self._save_secrets()
            if conn_handle == self._conn_handle:
                self._reset()
        elif event == _IRQ_GATTC_SERVICE_RESULT:
            conn_handle, start_handle, end_handle, uuid = data
            print('_IRQ_GATTC_SERVICE_RESULT:', conn_handle, start_handle, end_handle, uuid)
            if conn_handle == self._conn_handle and uuid == _UART_SERVICE_UUID:
                self._start_handle, self._end_handle = start_handle, end_handle
        elif event == _IRQ_GATTC_SERVICE_DONE:
            print('_IRQ_GATTC_SERVICE_DONE')
            if self._start_handle and self._end_handle:
                self._ble.gattc_discover_characteristics(self._conn_handle, self._start_handle, self._end_handle)
            else:
                print('Failed to find uart service.')
        elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
            conn_handle, def_handle, value_handle, properties, uuid = data
            print('_IRQ_GATTC_CHARACTERISTIC_RESULT:', conn_handle, def_handle, value_handle, properties, uuid)
            if conn_handle == self._conn_handle and uuid == _UART_RX_CHAR_UUID:
                self._rx_handle = value_handle
            if conn_handle == self._conn_handle and uuid == _UART_TX_CHAR_UUID:
                self._tx_handle = value_handle
        elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
            print('_IRQ_GATTC_CHARACTERISTIC_DONE')
            if self._tx_handle is not None and self._rx_handle is not None:
                self._ble.gap_pair(self._conn_handle)
            else:
                print('Failed to find uart rx characteristic')
        elif event == _IRQ_ENCRYPTION_UPDATE:
            conn_handle, encrypted, authenticated, bonded, key_size = data
            print('_IRQ_ENCRYPTION_UPDATE:', conn_handle, encrypted, authenticated, bonded, key_size)
        elif event == _IRQ_PASSKEY_ACTION:
            conn_handle, action, passkey = data
            print('_IRQ_PASSKEY_ACTION:', conn_handle, action, passkey)
            if action == _PASSKEY_ACTION_NUMCMP:
                accept = int(input('accept? '))
                print('_PASSKEY_ACTION_NUMCMP:', accept)
                self._ble.gap_passkey(conn_handle, action, accept)
            elif action == _PASSKEY_ACTION_DISP:
                print('_PASSKEY_ACTION_DISP: displaying 123456')
                self._ble.gap_passkey(conn_handle, action, 123456)
            elif action == _PASSKEY_ACTION_INPUT:
                passkey = int(input('passkey? '))
                print('_PASSKEY_ACTION_INPUT:', passkey)
                self._ble.gap_passkey(conn_handle, action, passkey)
            else:
                print('Unknown Action')
        elif event == _IRQ_SET_SECRET:
            sec_type, key, value = data
            key = sec_type, bytes(key)
            value = bytes(value) if value else None
            print('_IRQ_SET_SECRET:', key, value)
            if value is None:
                if key in self._secrets:
                    del self._secrets[key]
                    return True
                else:
                    return False
            else:
                self._secrets[key] = value
            return True
        elif event == _IRQ_GET_SECRET:
            sec_type, index, key = data
            print('_IRQ_GET_SECRET:', sec_type, index, bytes(key) if key else None)
            if key is None:
                i = 0
                for (t, _key), value in self._secrets.items():
                    if t == sec_type:
                        if i == index:
                            return value
                        i += 1
                return None
            else:
                key = sec_type, bytes(key)
                return self._secrets.get(key, None)
        elif event == _IRQ_GATTC_WRITE_DONE:
            conn_handle, value_handle, status = data
            print('_IRQ_GATTC_WRITE_DONE:', conn_handle, value_handle, status)

    def _load_secrets(self):
        self._secrets = {}
        try:
            with open(self.secFile, 'r') as f:
                entries = json.load(f)
                for sec_type, key, value in entries:
                    self._secrets[sec_type, binascii.a2b_base64(key)] = binascii.a2b_base64(value)
        except:
            print('no secrets available')

    def _save_secrets(self):
        try:
            with open(self.secFile, 'w') as f:
                json_secrets = [
                    (sec_type, binascii.b2a_base64(key), binascii.b2a_base64(value))
                    for (sec_type, key), value in self._secrets.items()
                ]
                json.dump(json_secrets, f)
        except:
            print('failed to save secrets')

    # Returns true if we've successfully connected and discovered characteristics.
    def is_connected(self):
        return (
            self._conn_handle is not None
            and self._tx_handle is not None
            and self._rx_handle is not None
        )

    # Connect to the specified device (otherwise use cached address from a scan)
    def connect(self, addr_type=None, addr=None, callback=None):
        self._addr_type = addr_type or self._addr_type
        if self._addr_type == None:
            self._addr_type = 0
        addr = binascii.unhexlify(addr.strip().replace(':',''))
        self._addr = addr or self._addr
        if self._addr_type is None or self._addr is None:
            return False
        self._ble.gap_connect(self._addr_type, self._addr, 4000)
        return True

    # Disconnect from current device
    def disconnect(self):
        if not self._conn_handle:
            return
        self._ble.gap_disconnect(self._conn_handle)
        self._reset()

    # Send data over the UART
    def write(self, v, response=False):
        if not self.is_connected():
            print('not connected')
            return
        self._ble.gattc_write(self._conn_handle, self._rx_handle, v, 1 if response else 0)

@chriscmorgan
Copy link

@z7jzk, was wondering if you where able to get this working or not?
I was looking to implement bonding for my esp32-c3 device but have been struggling so far.

@z7jzk
Copy link
Author

z7jzk commented Nov 7, 2021

@chriscmorgan, sorry for the delay in my response. I ended up refactoring my entire project over to ESP-IDF directly. I struggled with older releases to get this to work, but was finally able to successfully pair and maintain the bond for repeated connections going forward starting with IDF version 4.3.1 about a month back. I suspect you may be able to get this to work with the latest firmware version on micropython.

@dpgeorge
Copy link
Member

Pairing and bonding is supported on esp32 since 5dbb822

Wind-stormger pushed a commit to BPI-STEAM/micropython that referenced this issue Oct 13, 2022
Cleanup `mpconfigboard.mk` of espressif boards
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants