BLE Fragment Transfer Scheduling Mechanisms in BitChat: A Deep Dive into the Mesh Stack Architecture
TL;DR: BitChat uses a layered mesh architecture where the Engine fragments large messages on a serial queue and enqueues them into bleQueue‑confined buffers, while the BLELinkLayer drains those buffers respecting radio back‑pressure, MTU limits, and BLE write‑ack timing to achieve deterministic, deadlock‑free fragment transfer.
BitChat's Bluetooth Low Energy (BLE) transport implements a sophisticated BLE fragment transfer scheduling system designed for reliable mesh communication. The architecture cleanly separates link‑layer radio management from higher‑level message processing, using strict queue confinement and back‑pressure mechanisms to prevent deadlocks and buffer overflows. This design is documented in the project's BLE architecture specification and implemented across the Swift source codebase.
The Three‑Layer Scheduling Architecture
BitChat's fragment transfer relies on three interacting components, each with distinct responsibilities and queue confinement guarantees.
BLELinkLayer: Radio Duty‑Cycle and Buffer Ownership
The BLELinkLayer owns all CoreBluetooth interaction, including advertising, scanning, connection scheduling, and MTU negotiation. Crucially, it maintains the back‑pressure buffers that mediate between the engine and the radio:
pendingPeripheralWrites— buffers for outbound writespendingNotifications— buffers for notification‑style transferspendingWriteBuffers— additional write buffering
These buffers are bleQueue‑confined, meaning only code running on bleQueue may access them. According to the architecture documentation, "bleQueue owns the link buffers" including pendingPeripheralWrites and pendingNotifications【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L95-L99】.
The link layer also enforces the radio duty‑cycle, ensuring that scanning and advertising windows respect power constraints while maintaining mesh connectivity.
Engine: Serial Queue Fragmentation
The Engine runs on a single serial queue (mesh.message) and owns all message‑level processing:
- Wire codec (serialization/deserialization)
- Fragmentation of large payloads
- Deduplication of received packets
- Peer registry state management
The architecture document explicitly lists "wire codec, fragmentation, dedup" as engine responsibilities【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L39-L41】. By performing fragmentation on the serial engine queue, BitChat guarantees that fragment creation and ordering are race‑free—no two messages interleave their fragments.
The Scheduling Contract: Enqueue, Don't Block
The engine never writes directly to CoreBluetooth. Instead, it follows a strict enqueue‑and‑return pattern:
- Slice the payload into MTU‑sized fragments
- Pass each fragment to
BLELinkLayer.enqueueFragment() - Return immediately, without waiting for radio transmission
This contract eliminates blocking across layers. The link layer asynchronously drains its buffers based on actual radio capacity, not engine demand.
Strict Sync‑Edge Ordering: Preventing Deadlocks
BitChat enforces a unidirectional synchronization order that guarantees progress:
main / test threads → engine → bleQueue → (optional crypto queues)
As documented in the architecture spec, "No component may block waiting for a downstream component," which ensures that fragment scheduling proceeds monotonically without circular waits【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L71-L74】.
The onEngine helper enforces that only the engine may synchronously call into bleQueue, never the reverse. This prevents any scenario where the link layer could block waiting for engine processing【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L56-L63】.
Fragment Dispatch Flow: Step by Step
The complete BLE fragment transfer scheduling sequence operates as follows:
| Step | Actor | Action |
|---|---|---|
| 1. Fragment creation | Engine | Slices large message into MTU‑sized fragments on mesh.message queue |
| 2. Enqueue | Engine | Calls BLELinkLayer.enqueueFragment(), placing data into pendingPeripheralWrites or pendingNotifications |
| 3. Back‑pressure drain | BLELinkLayer | drainWriteBuffersIfPossible() removes and sends fragments only while peripheralCanWrite() returns true |
| 4. Completion | Engine | Marks transfer complete after final fragment acknowledgment; may trigger next queued message |
The link layer's drain method respects peripheral‑advertised flow control. It does not speculate about capacity—it waits for explicit write‑ready indications from CoreBluetooth.
Code Implementation
Here's how the scheduling mechanism translates to Swift code, adapted from the BitChat source:
// MARK: - Engine side (mesh.message queue)
func sendLargeMessage(_ data: Data, to peer: BLEPeer) {
// 1️⃣ Fragment payload according to negotiated MTU
let fragments = BLEFragmenter.fragment(data, mtu: currentMTU)
// 2️⃣ Enqueue each fragment for link-layer scheduling
fragments.forEach { fragment in
// Engine never blocks; returns immediately after enqueue
bleLinkLayer.enqueueFragment(fragment, for: peer)
}
}
// MARK: - BLELinkLayer side (bleQueue)
func enqueueFragment(_ fragment: Data, for peer: BLEPeer) {
// Buffer is bleQueue-confined; no locking needed
pendingPeripheralWrites.append((fragment, peer))
drainWriteBuffersIfPossible()
}
// MARK: - Back-pressure aware draining
func drainWriteBuffersIfPossible() {
while let (fragment, peer) = pendingPeripheralWrites.first,
peripheralCanWrite(peer) { // Respects BLE flow-control
peripheralWrite(fragment, to: peer)
pendingPeripheralWrites.removeFirst()
}
// If peripheralCanWrite() returns false, loop exits naturally
// and awaits next CoreBluetooth callback
}
Test Verification: Back‑Pressure Behavior
The test suite validates the scheduling contract under constrained conditions. In BLEServiceTests.swift, the test "fragment‑transfer respects back‑pressure" creates a mock peripheral that accepts only a limited number of concurrent writes, then verifies that the engine correctly pauses until the peripheral signals readiness【/cache/repos/github.com/permissionlesstech/bitchat/main/bitchatTests/BLEServiceTests.swift#L120-L140】.
This test confirms that the BLE fragment transfer scheduling mechanism does not overflow peripheral buffers or drop fragments under radio congestion.
Key Source Files
| File | Purpose |
|---|---|
docs/BLE-ARCHITECTURE-V3.md |
Complete design specification including scheduling contracts and queue confinement rules【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L31-L38】 |
bitchatTests/BLEServiceTests.swift |
Unit tests for fragment creation, enqueuing, and back‑pressure handling【/cache/repos/github.com/permissionlesstech/bitchat/main/bitchatTests/BLEServiceTests.swift#L120-L140】 |
bitchat/BLEService+LinkLayerCentralRole.swift |
Link‑layer central implementation with buffer management |
bitchat/BLEService+LinkLayerPeripheralRole.swift |
Link‑layer peripheral implementation |
Summary
- Layer separation: Engine handles fragmentation on
mesh.message; BLELinkLayer handles radio scheduling onbleQueue - Buffer confinement:
pendingPeripheralWrites,pendingNotifications, andpendingWriteBuffersare bleQueue‑confined, eliminating race conditions - Enqueue‑and‑return: Engine never blocks on radio I/O; fragments are asynchronously drained based on actual peripheral capacity
- Sync‑edge ordering: Unidirectional
engine → bleQueuecall pattern prevents deadlocks and guarantees monotonic progress - Back‑pressure respect: Link layer drains buffers only when
peripheralCanWrite()permits, matching fragment scheduling to BLE radio reality
Frequently Asked Questions
How does BitChat prevent fragment loss during BLE congestion?
The BLELinkLayer maintains bleQueue‑confined buffers (pendingPeripheralWrites, pendingNotifications) that absorb fragments from the engine. The drain loop only executes writes while peripheralCanWrite() returns true, automatically pausing when the peripheral reports congestion. This back‑pressure mechanism ensures fragments remain queued in memory rather than being dropped at the radio interface.
Why does BitChat use a serial queue for fragmentation instead of concurrent queues?
Fragmentation runs on the single mesh.message queue to guarantee deterministic fragment ordering and eliminate race conditions. Since the engine also handles deduplication and peer registry updates, serial execution ensures that a message's fragments are created, enqueued, and tracked as an atomic logical unit without interleaving fragments from other messages.
What happens if a peripheral never acknowledges a fragment write?
The architecture's sync‑edge ordering prevents the engine from blocking on acknowledgment. The link layer owns timeout and retry logic within bleQueue—if a peripheral stalls, the buffer holds pending fragments while the link layer manages reconnection or error propagation via LinkEvents. The engine continues processing other messages without deadlock.
Can the engine overflow the link layer's buffers?
No. The enqueue operation is non‑blocking and returns immediately, but the buffers themselves are bounded by available memory. The engine's rate of fragment production is naturally throttled by the serial queue's processing speed, while the link layer's drain loop ensures fragments exit as radio capacity permits. Unbounded growth would indicate a bug (peripheral never reading) rather than a scheduling flaw.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →