How Bitchat Android Manages Peer Discovery Over Bluetooth Low Energy: A Technical Deep Dive
Bitchat Android discovers BLE peers through a layered architecture where BluetoothMeshService orchestrates advertising and scanning via BluetoothConnectionManager, maps MAC addresses to peer IDs using BluetoothConnectionTracker, and maintains reachability status through PeerManager updates triggered by ANNOUNCE packets.
The Bitchat Android mesh networking stack implements peer discovery over Bluetooth Low Energy using a modular, transport-agnostic architecture that mirrors its iOS implementation while leveraging Android’s native BLE APIs. This design separates concerns between transport coordination, GATT server/client management, and peer identity tracking to enable reliable off-grid communication without centralized infrastructure.
Transport-Level Orchestration with BluetoothMeshService
The discovery process begins in BluetoothMeshService.kt, which acts as the top-level BLE coordinator. This service instantiates the core BLE transport and registers it with the broader mesh system.
When initialized, BluetoothMeshService creates a BluetoothConnectionManager instance—the BLE "core"—and registers it with TransportBridgeService under the transport ID "BLE." This registration allows the mesh router to treat BLE as a first-class transport alongside other potential interfaces.
// From BluetoothMeshService.kt - initialization sequence
val connectionManager = BluetoothConnectionManager(context, this)
// Register with the transport bridge for mesh routing
transportBridgeService.registerTransport("BLE", connectionManager)
The service also injects a critical closure called isPeerDirectlyConnected that reads the live addressPeerMap from the connection tracker, enabling the UI to display real-time direct-connection status without polling.
Advertising and Scanning via Dual GATT Managers
Inside BluetoothConnectionManager.kt, the heavy lifting of peer discovery over Bluetooth Low Energy is handled by two specialized managers that run concurrently:
BluetoothGattServerManager handles BLE advertising, exposing the local device to remote scanners, and accepts inbound GATT connections from peers who discover the advertisement.
BluetoothGattClientManager performs continuous scanning for remote advertisements and initiates outbound GATT connections when compatible peers are detected.
Both managers are instantiated during BluetoothConnectionManager construction and activated via startServices():
// From BluetoothConnectionManager.kt
fun startServices() {
if (debugSettings.gattServerEnabled.value) {
gattServerManager.start()
}
if (debugSettings.gattClientEnabled.value) {
gattClientManager.startScanning()
}
}
The entire discovery mechanism is gated by debug-controlled flags—bleEnabled, gattServerEnabled, and gattClientEnabled—allowing developers to toggle transport functionality without reconstructing the service instance.
Mapping BLE Addresses to Peer Identities
Once physical connections establish, BluetoothConnectionTracker maintains the critical bi-directional mapping between network-layer identities and link-layer addresses. It stores this data in addressPeerMap, which associates a remote device’s MAC address with the peer-ID derived from the cryptographic Noise handshake.
When BluetoothConnectionManager receives a packet from the underlying BLE stack, it performs the following sequence:
- Extracts the RSSI value from the connection metadata.
- Updates the
PeerManagerviadelegate?.onRSSIUpdated()to signal link quality. - Forwards the packet to the central
PacketProcessorfor message handling. - Updates
addressPeerMapif this is a new or returning peer.
// Packet receipt handling in BluetoothConnectionManager.kt
override fun onPacketReceived(device: BluetoothDevice, data: ByteArray, rssi: Int) {
val peerId = connectionTracker.getPeerIdForAddress(device.address)
delegate?.onRSSIUpdated(peerId, rssi)
packetProcessor.processPacket(data)
}
Peer Lifecycle Management and Direct Link Detection
Discovery extends beyond mere connectivity into application-level peer management through PeerManager, which maintains a ConcurrentHashMap<String, PeerInfo> storing every announced peer in the mesh.
Processing ANNOUNCE Packets
When the mesh receives an ANNOUNCE packet (processed by DirectLinkAnnouncementPolicy), the system evaluates whether the advertised relay address represents a direct BLE link. If the relay address matches a currently connected MAC address in addressPeerMap, PeerManager invokes connectionManager.observePeerIfCurrent() to log the direct route and trigger immediate synchronization.
// From BluetoothMeshService.kt - handling incoming announcements
fun handleAnnounce(packet: AnnouncePacket) {
if (connectionManager.isDirectConnection(packet.relayAddress)) {
peerManager.observePeerIfCurrent(packet.peerId, packet.relayAddress)
triggerSync(packet.peerId)
}
}
Real-Time Connection Status
To support UI indicators showing whether a peer is reachable via direct BLE versus multi-hop routing, BluetoothMeshService injects a closure into PeerManager during initialization. This closure reads the live addressPeerMap, ensuring the displayed connection status always reflects the current link state without requiring additional network queries.
Practical Implementation: Starting Discovery and Broadcasting
To activate peer discovery over Bluetooth Low Energy in your own build, interact with the public API surface of BluetoothMeshService:
// 1️⃣ Start the BLE transport (normally called from the Activity lifecycle)
val meshService = BluetoothMeshService(context)
meshService.startServices() // Launches GATT server/client and registers transport
// 2️⃣ Toggle BLE discovery via debug settings without service recreation
com.bitchat.android.ui.debug.DebugSettingsManager
.getInstance()
.bleEnabled.value = true // Set false to pause scanning/advertising
// 3️⃣ Manually initiate connection to a specific device (debugging utility)
meshService.connectionManager.connectToAddress("AA:BB:CC:DD:EE:FF")
// 4️⃣ Broadcast a message to all discovered peers
val packet = BitchatPacket(
version = 1u,
type = MessageType.MESSAGE.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = SpecialRecipients.BROADCAST,
timestamp = System.currentTimeMillis().toULong(),
payload = "Hello, mesh!".toByteArray(),
ttl = MAX_TTL
)
meshService.broadcastRoutedPacket(RoutedPacket(packet))
Summary
- BluetoothMeshService coordinates the BLE transport by creating
BluetoothConnectionManagerand registering it with the mesh bridge under the ID "BLE." - Dual GATT architecture separates advertising (
BluetoothGattServerManager) from scanning (BluetoothGattClientManager), both controlled via debug flags inBluetoothConnectionManager.startServices(). - Address-to-identity mapping occurs in
BluetoothConnectionTrackerthroughaddressPeerMap, which links MAC addresses to Noise-derived peer IDs and tracks RSSI values. - Peer state management uses
PeerManagerwith aConcurrentHashMapto store discovered nodes, processing ANNOUNCE packets viaDirectLinkAnnouncementPolicyto distinguish direct BLE links from routed paths. - Dynamic status updates are enabled by a closure injected into
PeerManagerthat reads the live connection map, allowing the UI to reflect real-time reachability without network overhead.
Frequently Asked Questions
How does Bitchat Android distinguish between direct BLE links and routed connections?
The system checks the addressPeerMap maintained by BluetoothConnectionTracker. When processing an ANNOUNCE packet, DirectLinkAnnouncementPolicy verifies if the advertised relay address exists as a connected MAC address. If present, PeerManager marks the peer as directly connected via the closure injected by BluetoothMeshService; otherwise, the traffic routes through intermediate mesh hops.
What controls whether the app advertises or scans for peers?
Debug-controlled boolean flags—bleEnabled, gattServerEnabled, and gattClientEnabled—govern the discovery state. These are checked inside BluetoothConnectionManager.startServices(), allowing developers to toggle advertising, scanning, or the entire transport without destroying the BluetoothMeshService instance.
How does the app handle RSSI updates for discovered peers?
When BluetoothConnectionManager receives a packet from the BLE stack, it extracts the RSSI value and immediately notifies PeerManager via the delegate?.onRSSIUpdated() callback. This updates the link quality metric stored in the peer’s PeerInfo record, which the UI can access to display signal strength indicators.
Which source files contain the core BLE discovery logic?
The primary files are:
BluetoothMeshService.kt– Transport registration and ANNOUNCE handling.BluetoothConnectionManager.kt– GATT manager orchestration and packet routing.BluetoothGattServerManager.kt– BLE advertising and inbound connections.BluetoothGattClientManager.kt– Scanning and outbound connections.BluetoothConnectionTracker.kt– MAC-to-peer-ID mapping viaaddressPeerMap.PeerManager.kt– Central peer storage and direct-connection status.
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 →