How the BitChat BLE Service Coordinates CoreBluetooth Callbacks for Mesh Networking

BitChat's BLEService funnels all CoreBluetooth delegate callbacks through a dedicated serial queue, translates low-level Bluetooth events into high-level link events, and maintains thread-safe state through BLELinkStateStore to coordinate the mesh engine.

The BLEService class in the permissionlesstech/bitchat repository serves as the central coordination hub between Apple's CoreBluetooth framework and BitChat's internal mesh engine. By implementing a strict serial dispatch architecture and consolidating all delegate callbacks into a single extension, the service ensures deterministic processing of BLE events while preventing race conditions in peer-to-peer messaging. Understanding how this BLE service coordinates CoreBluetooth callbacks reveals the architectural patterns that enable reliable offline mesh communication.

Serial Queue Architecture for Thread Safety

All CoreBluetooth operations in BitChat execute on a dedicated serial queue to guarantee thread safety and deterministic ordering of state changes.

In BLEService.swift at lines 71-84, the service initializes bleQueue as a user-initiated DispatchQueue:

// BLEService.swift#L71-L84
private let bleQueue: DispatchQueue

init(...) {
    self.bleQueue = DispatchQueue(label: "com.bitchat.ble", qos: .userInitiated)
    // ...
    self.centralManager = CBCentralManager(delegate: self, queue: bleQueue)
}

By passing bleQueue to the CBCentralManager initializer, every delegate method—from state changes to characteristic updates—executes on this serial queue rather than the main thread. This design prevents concurrent access to shared link state and eliminates the need for complex locking mechanisms throughout the callback handlers.

Centralized Delegate Implementation

The service implements both CBCentralManagerDelegate and CBPeripheralDelegate protocols in a single file extension to consolidate all Bluetooth event handling logic.

BLEService+LinkLayerCentralRole.swift (lines 21-34) contains the extension header that adopts these protocols:

// BLEService+LinkLayerCentralRole.swift#L21-L34
extension BLEService: CBCentralManagerDelegate, CBPeripheralDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        // Handle power state changes
    }
    
    func centralManager(_ central: CBCentralManager, 
                       didDiscover peripheral: CBPeripheral, 
                       advertisementData: [String: Any], 
                       rssi RSSI: NSNumber) {
        // Handle peripheral discovery
    }
    // ... additional delegate methods
}

This centralized approach ensures that discovery, connection, service exploration, characteristic updates, and write confirmations all flow through a single code path before reaching the mesh engine.

State Management and Event Translation

The service bridges the gap between CoreBluetooth's asynchronous callbacks and the mesh engine's serial processing queue through a two-phase coordination strategy.

Upon receiving any Bluetooth event, the service immediately updates BLELinkStateStore—the single source of truth for physical link status. According to the source code at lines 45-59 of BLEService+LinkLayerCentralRole.swift, callbacks write peripheral connection states, discovered characteristics, and notification assembler status into this thread-safe store:

// BLEService+LinkLayerCentralRole.swift#L45-L59
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    guard !isPanicSuspended else { 
        cleanupLinkState(for: peripheral)
        return 
    }
    
    linkStateStore.updateConnectionState(peripheral, state: .connected)
    peripheral.discoverServices([bitchatServiceUUID])
}

The mesh engine accesses this store via a read-only port, ensuring that state queries never block the Bluetooth processing thread.

After updating the link state store, callbacks translate low-level Bluetooth events into semantic "link events" that the mesh engine consumes. At lines 81-86, the service calls emitLinkEvent(_:) to forward processed events to the engine's messageQueue:

// BLEService+LinkLayerCentralRole.swift#L81-L86
private func emitLinkEvent(_ event: LinkEvent) {
    messageQueue.async { [weak self] in
        self?.engine?.processLinkEvent(event)
    }
}

This separation keeps the bleQueue lightweight—performing only BLE bookkeeping—while the cryptographic, routing, and deduplication work happens on the engine's dedicated queue.

Radio Control and Connection Policies

The service delegates scanning and connection policy decisions to BLERadioController to maintain clean separation between protocol handling and connection management.

At lines 84-100 of BLEService+LinkLayerCentralRole.swift, callbacks forward status changes to the radio controller:

// BLEService+LinkLayerCentralRole.swift#L84-L100
func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .poweredOn:
        radio.startScanning()
    case .poweredOff:
        radio.handlePowerOff()
    @unknown default:
        radio.handleErrorState(central.state)
    }
}

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

The radio object manages backoff timers, background wake-up triggers, and connection pooling without blocking the delegate callbacks.

Data Handling and Flow Control

Managing incoming data streams and outgoing write operations requires additional coordination to prevent buffer overflows and ensure complete frame delivery.

Notification Reassembly

When peripherals send characteristic notifications, the service buffers chunks using NotificationStreamAssembler until complete frames arrive. Lines 110-130 of BLEService+LinkLayerCentralRole.swift handle this assembly:

// BLEService+LinkLayerCentralRole.swift#L110-L130
func peripheral(_ peripheral: CBPeripheral, 
                didUpdateValueFor characteristic: CBCharacteristic, 
                error: Error?) {
    guard let data = characteristic.value, !data.isEmpty else { return }
    
    if let frame = bufferNotificationChunk(data, from: peripheral) {
        let packet = BitchatPacket(data: frame)
        emitLinkEvent(.frameDecoded(packet, from: peripheral.identifier))
    }
}

Once assembly completes, the service emits a .frameDecoded event to the engine, decoupling Bluetooth byte streams from packet processing logic.

Write Back-Pressure Management

The service manages transmit back-pressure by monitoring the peripheral's readiness signals. When CoreBluetooth invokes peripheralIsReady(toSendWriteWithoutResponse:), the service drains queued writes:

// BLEService+LinkLayerCentralRole.swift#L80-L87
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
    drainPendingWrites(for: peripheral)
}

func peripheral(_ peripheral: CBPeripheral, 
                didWriteValueFor characteristic: CBCharacteristic, 
                error: Error?) {
    if let error = error {
        SecureLogger.error("Write failed: \(error)")
    } else {
        SecureLogger.debug("Write confirmed")
    }
}

Write errors are logged but not retried automatically, maintaining deterministic flow control that prevents infinite retry loops in mesh networks.

Error Handling and Recovery

The service implements defensive checks at the entry point of every callback to handle panic states gracefully. At lines 44-47, callbacks verify the isPanicSuspended flag before processing:

// BLEService+LinkLayerCentralRole.swift#L44-L47
guard !isPanicSuspended else {
    cleanupLinkState(for: peripheral)
    return
}

If a panic reset is in progress, the service aborts the operation and sanitizes the link state to prevent inconsistent state propagation during recovery scenarios.

Summary

  • Serial Queue Isolation: All CoreBluetooth callbacks execute on a dedicated bleQueue to ensure thread-safe state management without locks.
  • Centralized Delegation: BLEService+LinkLayerCentralRole.swift consolidates both CBCentralManagerDelegate and CBPeripheralDelegate implementations for unified event handling.
  • State Store Pattern: BLELinkStateStore serves as the single source of truth for link status, updated synchronously during callbacks and accessed read-only by the mesh engine.
  • Event Translation: Low-level Bluetooth events convert to high-level LinkEvent types via emitLinkEvent(_:), processed on the engine's separate messageQueue.
  • Radio Abstraction: BLERadioController manages scanning policies and connection back-off, keeping delegate methods focused on protocol handling.
  • Defensive Recovery: Panic guards in every callback entry point enable graceful degradation during BLE stack resets.

Frequently Asked Questions

How does BitChat prevent race conditions in BLE callbacks?

BitChat prevents race conditions by funneling all CoreBluetooth delegate callbacks through a single serial DispatchQueue defined in BLEService.swift. By initializing CBCentralManager with this specific queue (lines 71-84), every discovery, connection, and characteristic update executes sequentially, eliminating the need for locks when accessing shared state stores.

What happens when a BLE peripheral sends fragmented data?

The BLEService buffers incoming notification chunks using NotificationStreamAssembler within the peripheral(_:didUpdateValueFor:error:) callback (lines 110-130). Once the assembler detects a complete frame, it converts the raw bytes into a BitchatPacket and emits a .frameDecoded event to the mesh engine, ensuring that packet reconstruction happens before cryptographic validation.

Why does BitChat separate BLE callback processing from mesh engine processing?

The separation maintains responsiveness in the Bluetooth stack while allowing complex mesh operations to proceed without deadline pressure. By translating Bluetooth events into LinkEvent types and dispatching them to the engine's messageQueue, the bleQueue remains available for immediate ACK responses and prevents deadlocks during cryptographic operations that might block for milliseconds.

How does the service handle Bluetooth power state changes?

The centralManagerDidUpdateState callback forwards power transitions to BLERadioController (lines 84-100), which implements the policy logic for starting or stopping scans. This abstraction allows the service to react immediately to .poweredOn events while the radio controller manages backoff timers and background wake-up sequences independently.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →