# How BitChat Implements BLE Central and Peripheral Roles for Mesh Messaging

> Discover how BitChat's BLE service uses central and peripheral roles for seamless peer-to-peer mesh messaging with thread-safe event handling.

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

---

**BitChat’s `BLEService` class operates simultaneously as both a Bluetooth Low Energy peripheral (advertising and accepting subscriptions) and central (scanning and initiating connections) to enable direct peer-to-peer mesh networking with deterministic, thread-safe event handling.**

BitChat is an open-source messaging protocol designed for permissionless, offline-first communication using Bluetooth Low Energy (BLE). The iOS implementation achieves this through a dual-role BLE architecture where every device can broadcast availability while actively seeking peers. The `BLEService` class manages these concurrent responsibilities by delegating physical layer operations to specialized extensions that emit pure `BLELinkEvent` objects consumed by a separate mesh engine.

## Architectural Separation: Link Layer vs. Mesh Engine

BitChat’s BLE transport follows the **BLE‑ARCHITECTURE‑V3** design documented in [`BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/BLE-ARCHITECTURE-V3.md). This architecture isolates CoreBluetooth interactions from protocol logic to ensure deterministic, lock‑free operation.

*   **`BLELinkLayer`** (implemented in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) and its extensions) is the sole module importing CoreBluetooth. It owns both the `CBCentralManager` and `CBPeripheralManager`, handling scanning, advertising, duty cycles, and raw back‑pressure buffers.
*   **`bleQueue`** is a dedicated serial dispatch queue where all peripheral and central callbacks execute. This confines physical layer mutations to a single thread, preventing race conditions during link‑state updates.
*   **Mesh Engine** runs on a separate `engineQueue` and processes pure protocol logic (packet fragmentation, deduplication, Noise handshakes, and routing). It receives events via `emitLinkEvent()` and never interacts with CoreBluetooth directly.

This separation allows the mesh engine to remain pure and testable while the link layer manages complex BLE lifecycle callbacks.

## Peripheral Role Implementation

The peripheral role implementation resides in `BLEService+LinkLayerPeripheralRole.swift` and conforms to `CBPeripheralManagerDelegate`. It advertises the BitChat service UUID, exposes a writable GATT characteristic, and forwards inbound packets to the mesh engine.

### Initialization and Advertising

When the peripheral manager state transitions to `.poweredOn`, the service constructs the GATT database and begins advertising.

```swift
// In BLEService+LinkLayerPeripheralRole.swift
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
    switch peripheral.state {
    case .poweredOn:
        peripheral.removeAllServices()
        let characteristic = CBMutableCharacteristic(
            type: BLEService.characteristicUUID,
            properties: [.notify, .write, .writeWithoutResponse, .read],
            value: nil,
            permissions: [.readable, .writeable]
        )
        let service = CBMutableService(type: BLEService.serviceUUID, primary: true)
        service.characteristics = [characteristic]
        peripheral.add(service)               // <-- triggers didAdd delegate
    default: /* handle other states */ 
    }
}

// Called after the service is added
func peripheralManager(_ peripheral: CBPeripheralManager,
                       didAdd service: CBService,
                       error: Error?) {
    guard error == nil else { return }
    peripheral.startAdvertising(BLERadioController.advertisementData())
}

```

### Managing Central Subscriptions

When a remote central subscribes to the characteristic, BitChat records the connection in the **link‑state store**, performs rate‑limiting via `BLESubscriptionAnnounceLimiter`, and schedules an announce packet.

```swift
func peripheralManager(_ peripheral: CBPeripheralManager,
                       central: CBCentral,
                       didSubscribeTo characteristic: CBCharacteristic) {
    let uuid = central.identifier.uuidString
    linkStateStore.addSubscribedCentral(central)

    // Rate‑limit announce packets to prevent flooding
    switch subscriptionAnnounceLimiter.decision(for: uuid, now: Date()) {
    case .allowed:
        engineScheduler.schedule(after: TransportConfig.blePostAnnounceDelaySeconds) {
            self.sendAnnounce(forceSend: true)
            self.flushDirectedSpool()
        }
    case .rateLimited(let backoff, _, let suppress):
        if suppress { return }
        // Still flush pending directed packets
        engineScheduler.schedule(after: TransportConfig.blePostAnnounceDelaySeconds) {
            self.flushDirectedSpool()
        }
    }
}

```

### Handling Unsubscribes

When a central unsubscribes (`peripheralManager(_:didUnsubscribeFrom:characteristic:)`), BitChat clears the central’s link state, drops pending notifications, and emits a `centralLinkEnded` event to trigger route recalculation in the mesh engine.

## Central Role Implementation

The central role implementation in `BLEService+LinkLayerCentralRole.swift` conforms to `CBCentralManagerDelegate`. It manages device discovery, connection lifecycle, and outbound packet transmission.

### Scanning and Discovery

Upon `.poweredOn` state, the central begins scanning for the BitChat service UUID.

```swift
// In BLEService+LinkLayerCentralRole.swift
func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .poweredOn:
        radio.startScanning()                // starts scanning for BLEService.serviceUUID
    default: /* handle other states */
    }
}

// Discovered a peripheral
func centralManager(_ central: CBCentralManager,
                    didDiscover peripheral: CBPeripheral,
                    advertisementData: [String: Any],
                    rssi RSSI: NSNumber) {
    radio.handleDiscovery(peripheral, advertisementData: advertisementData, rssi: RSSI)
}

```

### Connection Lifecycle

When the user or mesh engine initiates a connection, the central manager delegates the result to `centralManager(_:didConnect:)`, which stores the peripheral in the link‑state store, resets connection back‑off timers, and triggers service discovery.

```swift
func centralManager(_ central: CBCentralManager,
                    didConnect peripheral: CBPeripheral) {
    linkStateStore.markConnected(peripheral)
    peripheral.discoverServices([BLEService.serviceUUID])
}

```

On disconnection (`centralManager(_:didDisconnectPeripheral:error:)`), BitChat logs the peripheral ID, records any back‑off state for exponential retry logic, and caches the device identifier to enable **wake‑on‑proximity** reconnects when the peripheral re‑enters range.

### Sending Outbound Packets

The mesh engine schedules outbound packets on the `engineQueue`, which are then marshaled to the `bleQueue` for actual transmission:

```swift
// Engine creates a packet and pushes it to the BLE queue
engineScheduler.schedule {
    let data = packet.toBinaryData()
    // BLEQueue handles back‑pressure and writes
    pendingPeripheralWrites.enqueue(data, for: peripheral.identifier.uuidString)
}

```

This two‑queue approach prevents the mesh engine from blocking on BLE back‑pressure while ensuring thread‑safe access to CoreBluetooth APIs.

## Key Implementation Files

| File | Responsibility |
|------|---------------|
| [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) | Core service class, BLE constants, admission registry, and high‑level event routing between link layer and mesh engine. |
| `BLEService+LinkLayerPeripheralRole.swift` | Implements `CBPeripheralManagerDelegate` for advertising, subscription management, and notification dispatch. |
| `BLEService+LinkLayerCentralRole.swift` | Implements `CBCentralManagerDelegate` for scanning, connection handling, and disconnection recovery. |
| [`BLERadioController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLERadioController.swift) | Low‑level radio abstraction responsible for building advertisement payloads, duty‑cycle management, and tracking recent peripherals for wake‑on‑proximity. |
| [`BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/BLE-ARCHITECTURE-V3.md) | Comprehensive design document detailing concurrency contracts, MTU negotiation strategies, and the separation of concerns between physical and protocol layers. |

## Summary

*   **Dual‑role concurrency**: BitChat’s `BLEService` operates as both peripheral (via `CBPeripheralManager`) and central (via `CBCentralManager`) simultaneously, enabling true peer‑to‑peer mesh topology.
*   **Thread safety**: All CoreBluetooth callbacks execute on a dedicated `bleQueue`, while the mesh engine processes events on `engineQueue`, ensuring lock‑free, deterministic packet handling.
*   **State management**: The `linkStateStore` tracks subscriptions and connections, while `BLESubscriptionAnnounceLimiter` prevents announce‑packet flooding during high‑churn scenarios.
*   **Resilient connections**: The central role implements exponential back‑off and wake‑on‑proximity caching, while the peripheral role handles dynamic subscription lifecycles with proper cleanup on unsubscribe.

## Frequently Asked Questions

### Can a BitChat device act as both BLE central and peripheral at the same time?

Yes. BitChat initializes both a `CBCentralManager` and `CBPeripheralManager` within the `BLEService` class. According to the source code in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift), both managers operate concurrently on the `bleQueue`, allowing the device to advertise its presence while simultaneously scanning for and connecting to other peers. This dual‑role capability is essential for building resilient mesh networks where no single point of failure exists.

### What happens when a remote device subscribes to BitChat’s BLE characteristic?

When a central subscribes, the peripheral delegate in `BLEService+LinkLayerPeripheralRole.swift` invokes `peripheralManager(_:didSubscribeTo:characteristic:)`. This stores the central’s UUID in the link‑state store, checks the `BLESubscriptionAnnounceLimiter` to prevent rate‑limit violations, and schedules an announce packet via `sendAnnounce(forceSend:)`. If the rate limiter suppresses the announce, BitChat still flushes any pending directed packets from the spool to ensure message delivery.

### How does BitChat handle unexpected BLE disconnections?

The central role implements `centralManager(_:didDisconnectPeripheral:error:)` in `BLEService+LinkLayerCentralRole.swift` to handle disconnections. It records the peripheral ID in a recent‑peripheral cache for wake‑on‑proximity reconnects, updates the exponential back‑off state if the disconnect resulted from an error, and emits a link‑ended event to the mesh engine so routes can be recalculated. This recovery mechanism ensures temporary radio interference does not permanently sever mesh connectivity.

### Why does BitChat use separate queues for BLE operations and mesh engine processing?

BitChat uses `bleQueue` for all CoreBluetooth interactions and `engineQueue` for protocol logic to prevent thread starvation and race conditions. According to [`BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/BLE-ARCHITECTURE-V3.md), this separation allows the mesh engine to remain a pure, testable state machine that consumes `BLELinkEvent` objects, while the link layer handles asynchronous BLE callbacks and back‑pressure management on a dedicated serial queue. This architecture guarantees deterministic behavior during high‑throughput peer‑to‑peer communication.