← index #17805Issue #17476
Related · high · value 1.970
QUERY · ISSUE

BLE: Central Device cannot pair/bond Peripheral Device

openby NENightElvesopened 2025-08-01updated 2026-03-18
bugport-esp32

Port, board and/or hardware

esp32c3 (Also tested on esp32 as a central device)

MicroPython version

Tested on v1.24.1 and v1.25.0

Reproduction

I use the minimal code to test this. The peripheral device can be paired with nRF Connect.
Here is my code of peripheral device:

import bluetooth
from ble_util import bleconst
import time

print("peripheral mode")
handle = None
ble = bluetooth.BLE()


def irq(event, data):
    global handle, ble
    print("IRQ:", event, data)
    if event == bleconst.IRQ_CENTRAL_CONNECT:
        conn_handle, addr_type, addr = data
        handle = conn_handle
        print("    IRQ_CENTRAL_CONNECT", conn_handle, addr_type, addr)
    elif event == bleconst.IRQ_PASSKEY_ACTION:
        conn_handle, action, passkey = data
        print("    IRQ_PASSKEY_ACTION", conn_handle, action, passkey)
        if action == bleconst.PASSKEY_ACTION_DISPLAY:
            print("        PASSKEY_ACTION_DISPLAY")
            print("        Display passkey:", 123456)
            ble.gap_passkey(conn_handle, action, 123456)


ble.active(True)
ble.config(io=bleconst.IO_CAPABILITY_DISPLAY_ONLY,
           mitm=True,
           le_secure=True,
           bond=True)
ble.irq(irq)
ble.gap_advertise(0)
time.sleep(100)

When the peripheral device is connected with nRF Connect, the output is as below:

peripheral mode
IRQ: 1 (1, 1, <memoryview>)
    IRQ_CENTRAL_CONNECT 1 1 <memoryview>
IRQ: 19 (1, 3, <memoryview>)
IRQ: 27 (1, 6, 0, 500, 0)
IRQ: 27 (1, 36, 0, 500, 0)
IRQ: 29 (2, 0, None)
IRQ: 29 (1, 0, <memoryview>)
IRQ: 29 (2, 0, <memoryview>)
IRQ: 31 (1, 3, 0)
    IRQ_PASSKEY_ACTION 1 3 0
        PASSKEY_ACTION_DISPLAY
        Display passkey: 123456
IRQ: 28 (1, 1, 1, 1, 16)
IRQ: 30 (1, <memoryview>, <memoryview>)
IRQ: 29 (1, 0, <memoryview>)
IRQ: 30 (2, <memoryview>, <memoryview>)
IRQ: 29 (1, 0, <memoryview>)

And here is my code of central device:

import bluetooth
from ble_util import bleconst
import time

print("central mode")
handle = None
ble = bluetooth.BLE()


def irq(event, data):
    global handle, ble
    print("IRQ:", event, data)
    if event == bleconst.IRQ_PERIPHERAL_CONNECT:
        conn_handle, addr_type, addr = data
        handle = conn_handle
        print("    IRQ_PERIPHERAL_CONNECT", conn_handle, addr_type, addr)
        ble.gap_pair(conn_handle)
    elif event == bleconst.IRQ_PASSKEY_ACTION:
        conn_handle, action, passkey = data
        print("    IRQ_PASSKEY_ACTION", conn_handle, action, passkey)
        if action == bleconst.PASSKEY_ACTION_INPUT:
            print("        PASSKEY_ACTION_INPUT")
            print("        Input passkey:", 123456)
            ble.gap_passkey(conn_handle, action, 123456)


ble.active(True)
ble.config(io=bleconst.IO_CAPABILITY_KEYBOARD_ONLY,
           mitm=True,
           le_secure=True,
           bond=True)
ble.irq(irq)
ble.gap_connect(0, bytes.fromhex('123456789abc'))
time.sleep(100)

When connected to the peripheral device, I use gap_pair to pair/bond the peripheral device.
I run peripheral first, and then central. Here are the outputs:

Peripheral
=======================================
peripheral mode
IRQ: 1 (1, 0, <memoryview>)
    IRQ_CENTRAL_CONNECT 1 0 <memoryview>
IRQ: 29 (2, 0, None)
IRQ: 29 (1, 0, <memoryview>)
IRQ: 29 (2, 0, <memoryview>)
IRQ: 31 (1, 3, 0)
    IRQ_PASSKEY_ACTION 1 3 0
        PASSKEY_ACTION_DISPLAY
        Display passkey: 123456
IRQ: 28 (1, 0, 0, 0, 0)
##############################################
Central
========================================
central mode
IRQ: 7 (0, 0, <memoryview>)
    IRQ_PERIPHERAL_CONNECT 0 0 <memoryview>
IRQ: 29 (2, 0, <memoryview>)
IRQ: 29 (2, 0, None)
IRQ: 29 (1, 0, <memoryview>)
IRQ: 28 (0, 0, 0, 0, 0)

The result indicates that IRQ_PASSKEY_ACTION is not triggered in the central device, which is not as expected.

Expected behaviour

The IRQ_PASSKEY_ACTION should be triggered in the central device and then complete the pairing/bonding.

Observed behaviour

The IRQ_PASSKEY_ACTION is not triggered in the central device.

Additional Information

Is there any example of pairing/bonding between central device and peripheral device?
I saw there is examples/bluetooth/ble_bonding_peripheral.py, but there is no examples/bluetooth/ble_bonding_central.py.

Code of Conduct

Yes, I agree

CANDIDATE · ISSUE

unable to connect to nintendo switch joystick using bluetooth

closedby quantrpeteropened 2025-06-11updated 2025-06-11
bug

Port, board and/or hardware

waveshare c6 zero

MicroPython version

Hi
I am unable to use bluetooth to connect to nintendo switch joystick. The AI said:

The short answer is that, even though the ESP32-C6 hardware in the Waveshare C6 Zero is dual-mode BLE capable, MicroPython’s current ESP32 “ubluetooth” port only implements the Peripheral (server) role. Central-mode APIs like gap_scan() and gap_connect() are either stubbed out or not fully functional in the v1.25.0 firmware you’re running, so it will never actually discover or connect to a Joy-Con.

Is it true we don't have "central" mode for bluetooth?
thanks
Peter

Reproduction

import ubluetooth
from machine import Pin
import time
import struct

# Bluetooth constants
BLE_NINTENDO_VID = 0x057e  # Nintendo's Vendor ID (confirmed)
BLE_HID_SERVICE = "1812"   # HID Service UUID
BLE_SCAN_DURATION = 15000  # Scan for 15 seconds per cycle

# BLE event codes
IRQ_SCAN_RESULT = 5
IRQ_SCAN_DONE = 6
IRQ_CENTRAL_CONNECT = 1
IRQ_CENTRAL_DISCONNECT = 2
IRQ_GATTC_SERVICE_RESULT = 7
IRQ_GATTC_CHARACTERISTIC_RESULT = 8
IRQ_GATTC_READ_RESULT = 11
IRQ_GATTC_NOTIFY = 16

class BLEJoyCon:
    def __init__(self):
        self.ble = ubluetooth.BLE()
        self.ble.active(True)
        self.ble.irq(self.ble_irq)
        self.connected = False
        self.conn_handle = None
        self.hid_char_handle = None
        self.target_addr = None
        self.scan()

    def ble_irq(self, event, data):
        try:
            if event == IRQ_SCAN_RESULT:
                addr_type, addr, adv_type, rssi, adv_data = data
                addr = bytes(addr)
                addr_str = ':'.join(['%02x' % b for b in addr])
                vid_bytes = BLE_NINTENDO_VID.to_bytes(2, 'little')
                vid_detected = vid_bytes in adv_data
                # Print ALL advertisement data as hex for debugging
                #print("ADV DATA HEX:", [hex(b) for b in adv_data], "Address:", addr_str, "RSSI:", rssi)
                # Check for Joy-Con (R) specifically
                if b'Joy-Con (R)' in adv_data or vid_detected:
                    print("Found Nintendo Joy-Con (R):", adv_data)
                    self.target_addr = (addr_type, addr)
                    self.ble.gap_scan(None)
                    self.ble.gap_connect(addr_type, addr)

            elif event == IRQ_SCAN_DONE:
                print("Scan completed")
                if not self.connected and not self.target_addr:
                    print("No Nintendo controller found, attempting direct connection...")

            elif event == IRQ_CENTRAL_CONNECT:
                self.conn_handle, addr_type, addr = data
                self.connected = True
                addr_str = ':'.join(['%02x' % b for b in bytes(addr)])
                print("Connected to", addr_str)
                self.ble.gattc_discover_services(self.conn_handle)

            elif event == IRQ_CENTRAL_DISCONNECT:
                self.connected = False
                self.conn_handle = None
                self.target_addr = None
                print("Disconnected")
                self.scan()

            elif event == IRQ_GATTC_SERVICE_RESULT:
                conn_handle, start_handle, end_handle, uuid = data
                # Print ALL discovered service UUIDs for debugging
                print("Service found:", uuid)
                if conn_handle == self.conn_handle and uuid == ubluetooth.UUID(BLE_HID_SERVICE):
                    print("Found HID service")
                    self.ble.gattc_discover_characteristics(self.conn_handle, start_handle, end_handle)

            elif event == IRQ_GATTC_CHARACTERISTIC_RESULT:
                conn_handle, def_handle, value_handle, properties, uuid = data
                if conn_handle == self.conn_handle and "2A4D" in str(uuid):
                    self.hid_char_handle = value_handle
                    print("Found HID characteristic")
                    self.ble.gattc_write(self.conn_handle, self.hid_char_handle, b'\x01\x00', 1)

            elif event == IRQ_GATTC_NOTIFY:
                conn_handle, value_handle, notify_data = data
                if conn_handle == self.conn_handle and value_handle == self.hid_char_handle:
                    self.parse_hid_data(notify_data)
        except Exception as e:
            print("IRQ error:", e)

    def scan(self):
        print("Scanning for devices...")
        self.ble.gap_scan(BLE_SCAN_DURATION, 30000, 30000)

    def parse_hid_data(self, data):
        try:
            if len(data) >= 10:
                buttons = data[3]
                stick_x = (data[6] | (data[7] << 8)) & 0xFFF
                stick_y = (data[8] | (data[9] << 8)) & 0xFFF
                print("Buttons:", bin(buttons), "Stick X:", stick_x, "Stick Y:", stick_y)
                if buttons & 0x01:
                    print("A button pressed")
                if buttons & 0x02:
                    print("B button pressed")
                if buttons & 0x04:
                    print("X button pressed")
                if buttons & 0x08:
                    print("Y button pressed")
            else:
                print("Short HID report:", [hex(b) for b in data])
        except Exception as e:
            print("Error parsing HID data:", e)

    def stop(self):
        if self.connected:
            self.ble.gap_disconnect(self.conn_handle)
        self.ble.active(False)

# Initialize and run
try:
    joycon = BLEJoyCon()
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    joycon.stop()
    print("Stopped")
except Exception as e:
    print("Main error:", e)

Expected behaviour

Observed behaviour

Additional Information

No, I've provided everything above.

Code of Conduct

Yes, I agree

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied