# How BLEService Manages Simultaneous Bluetooth Peripheral and Central Roles in Bitchat

> Discover how BLEService handles simultaneous Bluetooth peripheral and central roles in Bitchat. Learn about its architecture for concurrent BLE operation.

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

---

**BLEService runs both Bluetooth Low Energy roles concurrently by maintaining separate CoreBluetooth managers on a dedicated serial queue, consolidating all link state in a unified store, and routing decoded packets through a single engine queue.**

The `BLEService` class in [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat) is the core mesh transport component that enables a single iOS device to act as both a **BLE peripheral** (advertising, accepting connections) and a **BLE central** (scanning, initiating connections) simultaneously. This dual-role architecture is essential for building a peer-to-peer mesh network where every node can both broadcast and receive messages. This article explains how `BLEService` implements simultaneous Bluetooth peripheral and central roles without race conditions or resource contention.

---

## Core Architecture: Separate Managers, Unified State

BLEService maintains **two distinct CoreBluetooth objects** that coexist on the same process:

- **`CBCentralManager`** — handles scanning for peripherals and initiating connections (central role)
- **`CBPeripheralManager`** — handles advertising services and managing subscriptions (peripheral role)

Both managers are created on the same serial dispatch queue (`bleQueue`) defined in [[`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) at lines 71–78. This queue serialization guarantees that peripheral and central callback events never race against each other:

```swift
// BLEService.swift (lines 71-78)
private lazy var bleQueue: DispatchQueue = {
    return DispatchQueue(label: "com.permissionlesstech.bitchat.ble", qos: .userInitiated)
}()

private lazy var messageQueue: DispatchQueue = {
    return DispatchQueue(label: "com.permissionlesstech.bitchat.engine", qos: .userInitiated)
}()

```

The **`messageQueue`** (also called the "engine queue") owns the mesh protocol state and consumes higher-level events emitted from `bleQueue`. This two-queue design separates Bluetooth hardware events from protocol logic, preventing deadlock while maintaining determinism.

---

## Link State Store: Treating All Connections Uniformly

All physical links—whether originated as a peripheral connection (central role) or accepted as a subscribed central (peripheral role)—are stored in a single **`BLELinkStateStore`**:

```swift
// BLEService.swift (lines 106-108)
private let linkStateStore = BLELinkStateStore()

```

This unified store records:

- **Peripheral-side connections**: centrals that have subscribed to the local characteristic
- **Central-side connections**: peripherals the local device has connected to

By treating both connection types identically, the mesh engine can route packets without knowing which role originated the link. The `linkStateStore` property appears at lines 106–108 in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift).

---

## Radio Controller: Shared Admission Policies

The **`BLERadioController`** (instantiated as the `radio` lazy property at lines 76–80) implements admission policies common to both roles:

- Discovery throttling
- Connection-slot budgeting
- Background-connect back-off
- Duty-cycle control

This controller is consulted from **both** the central-side callbacks (e.g., `didDiscover`) and the peripheral-side callbacks (e.g., `didAdd service`), ensuring consistent resource management regardless of role.

```swift
// BLEService.swift (lines 76-80)
private lazy var radio: BLERadioController = {
    return BLERadioController(queue: bleQueue, config: radioConfig)
}()

```

---

## Peripheral Role Implementation

The peripheral role logic lives in [`BLEService+LinkLayerPeripheralRole.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService+LinkLayerPeripheralRole.swift). Key responsibilities include:

**State handling**: When `peripheralManagerDidUpdateState` reports `.poweredOn`, the service creates the characteristic, adds the service, and starts advertising.

**Subscription tracking**: `didSubscribeTo` and `didUnsubscribeFrom` update `linkStateStore.addSubscribedCentral` and trigger a **rate-limited announce** to new centrals.

**Outbound back-pressure**: Packets are queued in `BLEOutboundNotificationBuffer`. The `peripheralManagerIsReady(toUpdateSubscribers:)` callback drains this buffer, applying back-pressure detection to prevent buffer overruns.

---

## Central Role Implementation

The central role logic lives in [`BLEService+LinkLayerCentralRole.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService+LinkLayerCentralRole.swift). Key responsibilities include:

**Power management**: `centralManagerDidUpdateState` starts scanning when powered on and tears down the peripheral-link store on power-off.

**Discovery**: `didDiscover` forwards advertisements to `radio.handleDiscovery` for admission control.

**Connection lifecycle**: 
- `didConnect` marks links as connected, discovers services, and records successful connections in the radio controller
- `didDisconnectPeripheral` records disconnects, releases slots, and restarts scanning

**Inbound data**: `didUpdateValueFor` buffers fragments, assembles them with `NotificationStreamAssembler`, and emits decoded frames via `emitLinkEvent`.

---

## Event Routing to the Engine

Both role extensions call **`emitLinkEvent(.frameDecoded(...))`** (or other `BLELinkEvent` variants). The engine queue picks these up, performs authentication and deduplication, and forwards the resulting `BitchatPacket` to the app via `BitchatDelegate`. This single entry point simplifies the higher-level protocol logic.

---

## Practical Usage Examples

### Starting the Dual-Role Transport

```swift
let bleService = BLEService(
    keychain: KeychainManager(),
    idBridge: NostrIdentityBridge(),
    identityManager: SecureIdentityStateManager()
)

bleService.startServices()   // Starts advertising AND scanning

```

The `startServices()` call forces both `CBCentralManager` and `CBPeripheralManager` instantiation via `initializeBluetoothManagersIfNeeded()` (lines 526–531 in [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)).

### Sending a Public Broadcast

```swift
bleService.sendMessage(
    "Hello mesh!",
    mentions: [],
    to: nil,                     // nil → public broadcast
    messageID: "msg-001",
    timestamp: Date()
)

```

This builds a `BitchatPacket`, signs it with the noise service, marks it as self-broadcast (to prevent re-processing), and calls `broadcastPacket(_:)` which writes to all connected peripherals and subscribed centrals (lines 845–886).

### Updating Identity Across Both Roles

```swift
bleService.setNickname("NewName")

```

The `setNickname` method (lines 1187–1192) updates the local identity store and forces an announce. This announce transmits over **both** active central links and any newly-subscribed centrals (peripheral role), ensuring identity propagation regardless of connection direction.

### Graceful Shutdown

```swift
bleService.stopServices()

```

`stopServices` (lines 1010–1045) sends a signed *leave* packet, drains pending notifications, cancels the maintenance timer, stops scanning and advertising, and disconnects all peripherals cleanly.

---

## Safety and Isolation Guarantees

All public APIs (`sendMessage`, `setNickname`, `startServices`, `stopServices`, etc.) schedule work on the appropriate queue and never block the UI. The service also guards against panic-recovery states via **`isPanicSuspended`** — a crash-reset will not leave the Bluetooth stacks in an inconsistent state.

---

## Summary

- **Dual CoreBluetooth managers** (`CBCentralManager` + `CBPeripheralManager`) on a single serial queue prevent race conditions
- **Unified `BLELinkStateStore`** treats peripheral and central links identically for routing simplicity  
- **`BLERadioController`** enforces shared admission policies across both roles
- **Separate `bleQueue` and `messageQueue`** isolate hardware events from protocol logic
- **Rate-limited announces and back-pressure buffers** ensure stable performance under load

---

## Frequently Asked Questions

### Can BLEService run both roles on any iOS device?

Yes. BLEService uses standard CoreBluetooth APIs available on all iOS devices that support Bluetooth Low Energy. The dual-role capability is implemented entirely in software—no special hardware is required. iOS allows simultaneous central and peripheral operation, though practical limits apply to connection count and advertising intervals.

### What happens if Bluetooth is turned off while BLEService is running?

Both `CBCentralManager` and `CBPeripheralManager` emit state update callbacks. The central role tears down the peripheral-link store on power-off, and the peripheral role stops advertising. The service enters a suspended state and will automatically restart both roles when Bluetooth powers back on, as handled by `centralManagerDidUpdateState` and `peripheralManagerDidUpdateState`.

### How does BLEService prevent the two roles from interfering with each other?

All CoreBluetooth callbacks execute on `bleQueue`, a serial dispatch queue. This serialization guarantees that peripheral events (subscriptions, ready-to-update signals) and central events (discoveries, connections, disconnections) are processed atomically with respect to each other. The `BLERadioController` provides additional coordination for radio resource allocation.

### What is the maximum number of simultaneous connections?

The practical limit depends on iOS Bluetooth stack constraints and the `BLERadioController` connection-slot budgeting. The radio controller actively manages available slots and applies back-off strategies for background connections. Exact limits are configurable via `radioConfig` passed to `BLERadioController` initialization.