BitChat BLE Stack Battery Optimization Strategies: 7 Power-Saving Mechanisms Explained
The BitChat BLE stack minimizes radio time through adaptive duty-cycled scanning, flood throttling, announce rate limiting, and RSSI-gated connection scheduling to preserve device battery life.
BitChat's Bluetooth Low Energy transport layer is engineered for longevity, combining multiple battery optimization strategies that keep the radio active only when necessary. These mechanisms work together to deliver a messaging experience that doesn't drain the device. Below are the seven core battery-saving techniques implemented in the permissionlesstech/bitchat repository.
Adaptive Duty-Cycled Scanning and Advertising
The cornerstone of BitChat's power efficiency is adaptive duty-cycled scanning, controlled by BLEScanDutyPolicy and executed by BLERadioController.
In bitchat/Services/BLE/BLEScanDutyPolicy.swift, the policy engine decides between continuous scanning and duty-cycled operation based on three inputs:
appIsActive— whether the UI is foregroundconnectedCount— number of active BLE linksforceContinuousScan— override flag for debugging or emergencies
// BLEScanDutyPolicy.swift#L5-L24
enum BLEScanDuty {
case continuous
case dutyCycle(onDuration: TimeInterval, offDuration: TimeInterval)
}
struct BLEScanDutyPolicy {
static func makePlan(
appIsActive: Bool,
connectedCount: Int,
forceContinuousScan: Bool
) -> BLEScanDuty {
if forceContinuousScan || (appIsActive && connectedCount > 0) {
return .continuous
}
// Default: 100ms on, 900ms off = 10% duty cycle
return .dutyCycle(onDuration: 0.1, offDuration: 0.9)
}
}
The BLERadioController then implements this plan using a timer that toggles scanning:
// BLERadioController.swift#L47-L55 and #L106-L115
class BLERadioController {
private var dutyTimer: Timer?
var dutyEnabled: Bool = true
func updateScanDutyPlan(_ plan: BLEScanDuty) {
dutyTimer?.invalidate()
switch plan {
case .continuous:
startScanning()
case .dutyCycle(let on, let off):
scheduleDutyCycle(onDuration: on, offDuration: off)
}
}
private func scheduleDutyCycle(onDuration: TimeInterval, offDuration: TimeInterval) {
// Turn on for burst, then schedule next window
startScanning()
dutyTimer = Timer.scheduledTimer(withTimeInterval: onDuration, repeats: false) { _ in
self.stopScanning()
Timer.scheduledTimer(withTimeInterval: offDuration, repeats: false) { _ in
self.scheduleDutyCycle(onDuration: onDuration, offDuration: offDuration)
}
}
}
}
This 10% default duty cycle reduces radio active time by 90% during background operation.
Flood and Battery Throttling
Large-scale broadcast operations — "floods" — are gated by battery-aware limits to prevent sustained high-power transmission. In bitchat/Services/BLE/BLEService.swift, the implementation throttles long fragment trains:
// BLEService.swift#L200 — region where battery limits apply
// "Flood throttling: defer bulk sends when battery is low"
A critical guard appears at line 5531, where scanning pauses during fragment transmission to avoid simultaneous TX/RX power drain.
Announce Throttling for Presence Broadcasts
Periodic presence announcements — the heartbeat of peer discovery — are strictly rate-limited by BLEAnnounceThrottle:
// BLEAnnounceThrottle.swift#L43-L52
struct BLEAnnounceThrottle {
let normalMinimumInterval: TimeInterval = 600 // 10 minutes
let forcedMinimumInterval: TimeInterval = 2 // 2 seconds urgent
private var lastSendTimes: [UUID: Date] = [:]
func shouldSend(force: Bool, now: Date) -> Bool {
let minimum = force ? forcedMinimumInterval : normalMinimumInterval
guard let last = lastSendTimes[currentPeer] else {
return true // Never sent before
}
return now.timeIntervalSince(last) >= minimum
}
}
Normal announcements are capped at one per 10 minutes per envelope. Urgent messages can force a 2-second minimum interval when necessary.
RSSI-Gated Connection Scheduling
BitChat prioritizes physically nearby peers to reduce transmission power and retry overhead. As documented in WHITEPAPER.md#L71:
"Connection scheduling is RSSI-gated with duty-cycled scanning to bound battery drain."
Stronger signals permit lower TX power, and nearby peers complete handshakes faster with fewer retransmissions.
Scan Pause During Long Fragment Trains
File transfers fragment large payloads across multiple BLE packets. During these trains, the stack suspends scanning entirely:
func sendLargeFile(_ file: Data) {
// BLEService.swift#L5531 — scan-pause guard
pauseScanning("fragment_train")
// ... fragment transmission ...
resumeScanning()
}
This eliminates the power penalty of simultaneous reception while transmitting.
Background Connect: Wake-on-Proximity
Even when suspended, BitChat can restore connectivity without continuous scanning. The BLERadioController header documents this mode:
// BLERadioController.swift#L21-L23
/// Wake-on-proximity: advertise a connectable beacon that wakes
/// the app when a peer with matching service UUID comes near.
/// No scanning required in deep sleep.
A nearby peer's advertisement triggers the system wake, allowing opportunistic reconnect without background radio operation.
Adaptive Power: Documented Strategy
The README.md#L48 entry ties these mechanisms together under the Adaptive Power feature:
Adaptive Power: Battery-optimized duty cycling — automatically reduces scanning aggressiveness based on app state, connected peers, and pending message urgency.
Practical Implementation Example
Here's how to configure the battery optimization stack for a typical messaging session:
import BitChat
class ChatViewController: UIViewController {
let bleService = BLEService.shared
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Foreground: allow continuous scanning if we have peers
let dutyPlan = BLEScanDutyPolicy.makePlan(
appIsActive: true,
connectedCount: bleService.currentLinkCount,
forceContinuousScan: false
)
bleService.radioController.updateScanDutyPlan(dutyPlan)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// Background: force duty cycling to save battery
let backgroundPlan = BLEScanDutyPolicy.makePlan(
appIsActive: false,
connectedCount: bleService.currentLinkCount,
forceContinuousScan: false
)
bleService.radioController.updateScanDutyPlan(backgroundPlan)
}
func sendUrgentMessage(_ content: Data) {
// Override announce throttle for priority delivery
if bleService.announceThrottle.shouldSend(force: true, now: Date()) {
bleService.broadcast(content, forceAnnounce: true)
}
}
}
Key Source Files for Battery Optimization
| File | Battery-Saving Role |
|---|---|
bitchat/Services/BLE/BLEScanDutyPolicy.swift |
Determines continuous vs. duty-cycled scanning |
bitchat/Services/BLE/BLERadioController.swift |
Executes duty-cycle timers, wake-on-proximity |
bitchat/Services/BLE/BLEAnnounceThrottle.swift |
Rate-limits presence announcements |
bitchat/Services/BLE/BLEService.swift |
Flood throttling, scan-pause during transfers |
WHITEPAPER.md |
RSSI-gated scheduling specification |
README.md |
Public feature documentation |
Summary
- Adaptive duty cycling reduces scanning to 10% of time in background through
BLEScanDutyPolicyandBLERadioController - Flood throttling prevents sustained high-power broadcasts during bulk transfers
- Announce throttling limits presence broadcasts to 10-minute intervals normally, 2 seconds when urgent
- RSSI-gated scheduling prefers nearby peers for lower TX power and faster handshakes
- Scan pause during fragments avoids TX/RX overlap during file transfers
- Wake-on-proximity enables background reconnect without continuous scanning
- Adaptive Power documentation unifies these strategies as a public feature commitment
Frequently Asked Questions
How much battery does BitChat's duty cycling actually save?
The default 10% duty cycle (100ms on, 900ms off) reduces BLE radio active time by 90% compared to continuous scanning. According to the BLERadioController implementation, this translates to substantial savings since BLE receive powertypically exceeds 10mA while the radio is active.
Can I force continuous scanning for debugging or emergencies?
Yes. Set forceContinuousScan: true when calling BLEScanDutyPolicy.makePlan(), or toggle the dutyEnabled flag in BLERadioController directly. This bypasses all duty cycling regardless of app state.
What triggers the 2-second urgent announce interval?
Pass force: true to BLEAnnounceThrottle.shouldSend(). This is intended for critical messages requiring rapid peer discovery, but frequent use will impact battery life proportionally.
Does RSSI-gated scheduling require additional hardware support?
No. The stack uses standard received signal strength indicators available in all BLE implementations. The white paper notes this is implemented in software scheduling logic within BLERadioController rather than hardware power control.
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 →