# How BitChat's BLE Peer Discovery Protocol Works: CoreBluetooth Implementation Guide

> Learn how BitChat's BLE peer discovery protocol works using CoreBluetooth. Discover service UUID filtering, RSSI evaluation, and budget-aware scheduling for efficient peer management.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-08

---

**BitChat discovers nearby peers using a layered BLE transport that separates the CoreBluetooth radio from the mesh engine, employing service UUID filtering, RSSI-based candidate evaluation, and a budget-aware scheduler to manage connections.**

The BitChat iOS application, hosted in the `permissionlesstech/bitchat` repository, implements a custom Bluetooth Low Energy (BLE) peer discovery protocol to locate and connect with nearby devices without internet infrastructure. This protocol functions through a strict architectural separation between the radio link layer and the mesh engine, enabling efficient background scanning while preventing radio deadlocks.

## BLE Radio Initialization and Scanning

The discovery process begins when the `BLEService` initializes the CoreBluetooth central manager. Once the Bluetooth radio powers on, the service transitions into scanning mode by invoking `BLERadioController.startScanning()`.

In `BLEService+LinkLayerCentralRole.swift`, the `centralManagerDidUpdateState` delegate method monitors the radio state and triggers scanning immediately upon reaching `.poweredOn` status:

```swift
// BLEService+LinkLayerCentralRole.swift#L84-L100
func centralManagerDidUpdateState(_ central: CBCentralManager) {
    if central.state == .poweredOn {
        radio.startScanning()
    }
}

```

The `startScanning()` implementation in [`BLERadioController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLERadioController.swift) configures the `CBCentralManager` to scan for peripherals advertising the BitChat service UUID. It explicitly enables *allow-duplicates* scanning to ensure rapid discovery of peers even if they advertise frequently:

```swift
// BLERadioController.swift#L73-L85
func startScanning() {
    centralManager.scanForPeripherals(
        withServices: [BLEService.serviceUUID],
        options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
    )
}

```

## Advertisement Reception and Candidate Creation

When the central manager discovers a peripheral advertising the target service UUID, CoreBluetooth delivers the advertisement through the `centralManager(_:didDiscover:advertisementData:rssi:)` delegate method.

This method immediately forwards the discovery data to the radio controller without processing logic:

```swift
// BLEService+LinkLayerCentralRole.swift#L39-L41
func centralManager(_ central: CBCentralManager, 
                    didDiscover peripheral: CBPeripheral, 
                    advertisementData: [String: Any], 
                    rssi RSSI: NSNumber) {
    radio.handleDiscovery(peripheral, advertisementData: advertisementData, rssi: RSSI)
}

```

The `BLERadioController.handleDiscovery` method constructs a `BLEConnectionCandidate` struct encapsulating the peripheral's identity, signal strength, name, connectable status, and timestamp. This candidate object serves as the unit of work for the connection scheduler:

```swift
// BLERadioController.swift#L57-L74
struct BLEConnectionCandidate {
    let peripheral: CBPeripheral
    let rssi: Int
    let name: String?
    let isConnectable: Bool
    let timestamp: Date
}

func handleDiscovery(_ peripheral: CBPeripheral, 
                     advertisementData: [String: Any], 
                     rssi: NSNumber) {
    let candidate = BLEConnectionCandidate(
        peripheral: peripheral,
        rssi: rssi.intValue,
        name: advertisementData[CBAdvertisementDataLocalNameKey] as? String,
        isConnectable: advertisementData[CBAdvertisementDataIsConnectable] as? Bool ?? false,
        timestamp: Date()
    )
    // Pass to scheduler...
}

```

## Connection Scheduling and Budget Enforcement

BitChat's BLE peer discovery protocol implements intelligent connection throttling through the `BLEConnectionScheduler`. When `handleDiscovery` receives a candidate, it delegates the connection decision to `BLEConnectionScheduler.handleDiscovery`, which evaluates multiple policy constraints:

*   **Connection budget**: Current count of active BLE links versus maximum allowed concurrent connections
*   **State deduplication**: Whether the peripheral already exists in the connection pool
*   **RSSI thresholds**: Minimum signal strength requirements for reliable mesh participation
*   **Traffic patterns**: Recent connection attempt history to prevent connection flapping

The scheduler returns an enum value that determines the candidate's fate. In [`BLERadioController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLERadioController.swift), a `switch` statement handles these outcomes:

```swift
// BLERadioController.swift#L80-L100
switch scheduler.handleDiscovery(candidate) {
case .ignore:
    return // Peripheral filtered by policy
case .retryLater:
    scheduleRetry(for: candidate) // Queue for future attempt
case .cancelStale:
    cancelExistingConnection(to: candidate.peripheral)
case .connectNow:
    beginCentralConnection(candidate) // Initiate BLE connection
}

```

## Connection Establishment and Service Discovery

When the scheduler returns `.connectNow`, `BLERadioController.beginCentralConnection` initiates the low-level BLE connection by calling `CBCentralManager.connect(peripheral)`. This transitions the peripheral from the *discovered* state to the *connecting* state.

Upon successful connection, CoreBluetooth invokes `centralManager(_:didConnect:)` in the link layer delegate. This callback updates the internal `BLELinkStateStore` and immediately triggers GATT service discovery to locate the BitChat characteristic:

```swift
// BLEService+LinkLayerCentralRole.swift#L62-L68
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    linkStateStore.updateState(for: peripheral, to: .connected)
    peripheral.discoverServices([BLEService.serviceUUID])
}

```

The subsequent characteristic discovery transforms the raw BLE connection into a *writable* mesh link. Once the service and characteristic UUIDs are verified, the link layer emits a `LinkEvent` to the mesh engine via the `emitLinkEvent` port, signaling that the peer is ready for Noise handshakes and packet transmission. This architectural separation is documented in [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md) (lines 31-36), which defines the **BLELinkLayer** responsibilities versus the mesh engine duties.

## Background Wake-on-Proximity

BitChat maintains a *recent-peripheral cache* to support background operation. When the application enters the background, disconnected peripherals remain cached with timestamps. If a cached peripheral returns to proximity and begins advertising, the protocol consults this cache to re-arm pending connections after a short ignore window expires.

This mechanism allows iOS to wake the application via Bluetooth background modes when previously-seen peers reappear, without requiring constant foreground scanning. The logic resides in the state update handlers within `BLEService+LinkLayerCentralRole.swift` around lines 84-96.

## Practical Implementation Examples

The following snippets demonstrate the public entry points for interacting with BitChat's BLE peer discovery protocol:

**Initiating scan mode:**

```swift
// Called from UI or app launch lifecycle
bleService.startScanning()  // Internally invokes BLERadioController.startScanning()

```

**Manual discovery injection (useful for unit testing):**

```swift
let peripheral: CBPeripheral = // ... obtained from CoreBluetooth
let advData: [String: Any] = [
    CBAdvertisementDataServiceUUIDsKey: [BLEService.serviceUUID],
    CBAdvertisementDataLocalNameKey: "BitChat-Demo"
]
let rssi: NSNumber = -45

// Manually feed discovery to bypass actual radio
bleRadioController.handleDiscovery(peripheral, 
                                    advertisementData: advData, 
                                    rssi: rssi)

```

**Post-connection service discovery (handled automatically by the SDK):**

```swift
// This occurs internally after centralManager(_:didConnect:)
peripheral.discoverServices([BLEService.serviceUUID])
// Followed by characteristic discovery once services are found

```

## Summary

*   BitChat's BLE peer discovery protocol operates through a layered architecture separating the CoreBluetooth radio (`BLERadioController`) from the mesh engine via the link layer abstraction.
*   Discovery relies on `CBCentralManager` scanning for specific service UUIDs with duplicate advertisement filtering enabled for rapid detection.
*   The `BLEConnectionScheduler` enforces connection budgets and RSSI policies to prevent resource exhaustion and connection flapping.
*   Successful candidates trigger GATT service discovery to establish writable mesh links, after which the engine handles Noise handshakes via `emitLinkEvent`.
*   Background operation is supported through a recent-peripheral cache that enables wake-on-proximity for returning peers.

## Frequently Asked Questions

### How does BitChat filter BLE advertisements to find valid peers?

BitChat filters advertisements by scanning exclusively for peripherals broadcasting the BitChat service UUID defined in `BLEService.serviceUUID`. When `centralManager(_:didDiscover:advertisementData:rssi:)` receives a packet, it validates the presence of this UUID in the advertisement data before creating a `BLEConnectionCandidate`, ensuring only compatible BitChat nodes enter the connection pipeline.

### What is the role of the BLEConnectionScheduler in peer discovery?

The `BLEConnectionScheduler` acts as a policy gatekeeper that determines whether a discovered peripheral should be ignored, queued for later, or connected immediately. It evaluates the current connection budget against `BLELinkStateStore` data, checks RSSI thresholds for signal quality, and analyzes recent traffic patterns to prevent duplicate connections to the same peer within short time windows.

### How does BitChat handle peer discovery when the app is in the background?

BitChat utilizes a recent-peripheral cache stored in `BLERadioController` that retains metadata about recently disconnected peers. When the app transitions to the background, this cache persists peripheral identifiers and timestamps. If a cached peer begins advertising again, the protocol recognizes the returning device and re-arms the connection attempt after a configurable ignore window, allowing iOS background Bluetooth triggers to wake the app.

### Where does the link layer send events once a peer is fully discovered?

Once service discovery completes and the BitChat GATT characteristic is verified, the link layer emits structured `LinkEvent` objects through the `emitLinkEvent` port. These events flow to the mesh engine, which then manages packet fragmentation, performs Noise protocol handshakes, and registers the peer in the active mesh table. This separation is architecturally defined in [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md).