Breadboard
docs
Example project · Board-to-board radio

Bluetooth LE: scan, connect, discover

Two ESP32-C6 boards run the full BLE story through the real vendor controller: one registers a Battery Service and advertises connectably, the other scans, hears the advertisement, connects as central over a genuine LE link, and discovers the server's GATT services. One script runs on both boards; each derives its role from its own MAC. Watch the serial tabs narrate both sides, follow ADV_IND / CONNECT_IND / ATT frames in the 📡 Sniffer, and click either board's Bluetooth tab for its live link state.

The Bluetooth LE: scan, connect, discover circuit as rendered by the simulator

How it works

The whole BLE client/server arc with two ESP32-C6 boards and one symmetric script: each board checks its own MAC's parity to pick a role. The even board registers a real GATT Battery Service (UUID 0x180F with a 0x2A19 level characteristic) and advertises connectably; the odd board scans, finds the advertiser, connects as central, and walks service discovery over the live LE link. The serial tabs narrate both sides: SRV advertising, CLI found, CLI connected, service UUID(0x180f), discovery done.

What's on the bench

  • 2× Battery
  • 2× ESP32-C6

How it's wired

  • Battery · posESP32-C6 · vin
  • Battery · negESP32-C6 · gnd
  • Battery · posESP32-C6 · vin
  • Battery · negESP32-C6 · gnd

The code

This MicroPython script runs on the emulated board every boot; edit it in the Code tab.

import bluetooth, time
b = bluetooth.BLE()
b.active(1)
server = b.config('mac')[1][-1] % 2 == 0
if server:
    S = (bluetooth.UUID(0x180F), ((bluetooth.UUID(0x2A19), bluetooth.FLAG_READ | bluetooth.FLAG_WRITE),))
    ((h,),) = b.gatts_register_services((S,))
    def cb(e, d):
        if e == 1:
            print('SRV ' + 'central connected')
        elif e == 2:
            print('SRV ' + 'central disconnected')
    b.irq(cb)
    b.gap_advertise(50000, b'\x02\x01\x06\x05\xffCAFE', connectable=True)
    print('SRV ' + 'advertising battery service')
else:
    tgt = []
    cn = []
    def cb(e, d):
        if e == 5 and not tgt:
            tgt.append((d[0], bytes(d[1])))
            print('CLI ' + 'found advertiser')
        elif e == 7:
            cn.append(d[0])
            print('CLI ' + 'connected')
        elif e == 9:
            print('CLI ' + 'service', d[3])
        elif e == 10:
            print('CLI ' + 'discovery done')
    b.irq(cb)
    b.gap_scan(0, 30000, 30000)
    while not tgt:
        time.sleep_ms(50)
    b.gap_scan(None)
    time.sleep_ms(50)
    b.gap_connect(tgt[0][0], tgt[0][1])
    while not cn:
        time.sleep_ms(50)
    time.sleep_ms(300)
    b.gattc_discover_services(cn[0])
    print('CLI discovering services')

Try this

  • Open both serial tabs and match the story line for line: SRV on one, CLI on the other.
  • Open the 📡 Sniffer: advertising PDUs on channel 37, then the connection’s data channel traffic.
  • Click each board's BLE Inspector tab: one shows advertising, the other connected as central.
  • Move on to the pairing and bonding examples: same bench, deeper into the stack.

Related projects