Before submitting
Firmware Version
2.7.1
What did you do?
Environment
- iPhone 14 (latest iOS)
- Heltec T114 v2 (BLE-only, no Wi‑Fi)
- Meshtastic firmware 2.7.7
- Meshtastic iOS app (latest App Store build)
- MQTT broker: Mosquitto (TLS optional)
Steps
- On the node, enabled MQTT uplink on the active channel (default public channel; downlink disabled).
- In the iOS app, enabled Client Proxy and configured the MQTT broker (host/port/creds; TLS optional).
- Verified in foreground that telemetry/messages from the node arrive on broker topics.
- Ensured iOS settings that maximize background continuity:
- Background App Refresh = ON (Meshtastic)
- Low Power Mode = OFF
- Bluetooth = ON
- Location permission = Always Allow
- App not force‑quit (left in app switcher).
- Sent the app to background and waited ~5–30+ minutes (also tested 60–120).
- Watched broker topics throughout the background interval.
- Brought the app back to foreground and watched topics again.
Observed outcome
- Topics update normally while in foreground.
- After the app sits in background, topics go quiet.
- Upon foregrounding, updates resume immediately.
Technical context / code pointers (what I looked at)
Goal: Explain why background forwarding stalls and where the code likely needs changes.
1) CoreBluetooth central has no state restoration
- Expected: initialize the central with a restoration identifier and handle
willRestoreState.
- Current pattern observed (representative):
// BLETransport.swift (representative)
centralManager = CBCentralManager(
delegate: delegate,
queue: .global(qos: .utility)
// ❌ no CBCentralManagerOptionRestoreIdentifierKey
)
// ... later ...
// ❌ Restoration callback exists but is commented or not wired:
/// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { ... }
2) MQTT client is process-lifetime bound
- CocoaMQTT
autoReconnect only helps while the process is active; iOS suspends background apps.
- Session settings aren’t background‑friendly:
cleanSession = true ⇒ broker discards subscriptions between connects.
keepAlive = 60 ⇒ stale links detected slowly in background mode.
3) Background wake sources not leveraged to re‑attach BLE/MQTT
- App already has location background mode. Use it (and BLE restoration) to trigger an opportunistic reconnect pipeline.
- Consider a small BGAppRefreshTask nudge.
Proposed fix (surgical and opt‑in)
Add an opt‑in setting: Background Gateway (beta). When ON, enable the following behavior.
A) Enable CoreBluetooth state preservation & restoration
// BLETransport.swift
private let kCentralRestoreID = "com.meshtastic.central"
centralManager = CBCentralManager(
delegate: delegate,
queue: .global(qos: .utility),
options: [CBCentralManagerOptionRestoreIdentifierKey: kCentralRestoreID]
)
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
os_log("BLE willRestoreState: %{public}@", log: bleLog, type: .info, String(describing: dict))
let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] ?? []
if peripherals.isEmpty {
central.scanForPeripherals(
withServices: [meshServiceUUID],
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
)
} else {
peripherals.forEach { central.connect($0, options: nil) }
}
BackgroundGateway.shared.resumeIfNeeded() // see C)
}
B) Make MQTT more resilient in background
// MqttClientProxyManager.swift
func configureForBackgroundGateway(_ enabled: Bool) {
if enabled {
mqtt.cleanSession = false // persist subs across reconnects
mqtt.keepAlive = 30 // detect stale faster (battery tradeoff)
} else {
mqtt.cleanSession = true
mqtt.keepAlive = 60
}
mqtt.autoReconnect = true
}
func reconnectIfNeeded() {
guard backgroundGatewayEnabled else { return }
guard mqtt.connState != .connected && mqtt.connState != .connecting else { return }
mqtt.connect()
}
// On successful connect ACK:
func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
// (Re)subscribe to topics needed by Client Proxy
topicsToSubscribe.forEach { mqtt.subscribe($0, qos: .qos1) }
}
C) Opportunistic wake handlers (BLE restore, location, BG refresh)
// BackgroundGatewayCoordinator.swift
import BackgroundTasks
enum BGID {
static let refresh = "com.meshtastic.gateway.refresh"
}
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGID.refresh, using: nil) { task in
BackgroundGateway.shared.reconnectIfNeeded { ok in
task.setTaskCompleted(success: ok)
BackgroundGateway.shared.scheduleNextRefresh()
}
}
}
func scheduleNextRefresh() {
let req = BGAppRefreshTaskRequest(identifier: BGID.refresh)
req.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(req)
}
// LocationsHandler.swift
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
BackgroundGateway.shared.reconnectIfNeeded() // opportunistic nudge
}
D) Info.plist additions
UIBackgroundModes: add bluetooth-central, location, fetch
BGTaskSchedulerPermittedIdentifiers: ["com.meshtastic.gateway.refresh"]
E) Logging
// OSLog categories
let bleLog = OSLog(subsystem: "com.meshtastic", category: "BLE")
let mqttLog = OSLog(subsystem: "com.meshtastic", category: "MQTT")
let bgLog = OSLog(subsystem: "com.meshtastic", category: "Background")
os_log("Attempting MQTT reconnect (bg)", log: mqttLog, type: .info)
Why opt‑in? Battery/OS‑throttling tradeoffs; keep default behavior unchanged for most users.
Expected Behavior
- With Client Proxy enabled and Background Gateway (beta) toggled ON, the app should, within iOS limits, best‑effort keep gatewaying in background:
- iOS delivers CoreBluetooth restoration callbacks; the app re‑attaches to the node (scan/known peripherals) automatically.
- On any background wake source (BLE restoration, location update, BG app refresh), the app attempts MQTT reconnect and re‑subscribe without user foregrounding.
- MQTT session parameters favor background resilience:
cleanSession = false (retain subs across reconnects)
keepAlive ≈ 30s (faster stale detection in bg)
- small backoff on repeated failures.
Acceptance (practical)
- After 30–60+ minutes in background, forwarding resumes automatically on the next system wake (no manual foregrounding).
- Reconnect latency after a wake ≤ ~30s.
- When the feature is OFF (default), no behavior/battery regressions.
Code‑level expectations (what the app should do)
CoreBluetooth restoration path
// central init has a restoration ID
CBCentralManagerOptionRestoreIdentifierKey = "com.meshtastic.central"
// restoration delegate is implemented and wired
func centralManager(_ c: CBCentralManager, willRestoreState dict: [String : Any]) {
// resume scan/connect and then nudge MQTT
BackgroundGateway.shared.resumeIfNeeded()
}
MQTT reconnection path on wakes
// On BLE restore / location update / BG refresh:
BackgroundGateway.shared.reconnectIfNeeded()
// Reconnect will connect + re‑subscribe when not connected:
mqtt.cleanSession = false
mqtt.keepAlive = 30
mqtt.connect()
Info.plist / background config
UIBackgroundModes: bluetooth-central, location, fetch
BGTaskSchedulerPermittedIdentifiers: com.meshtastic.gateway.refresh
Logging
- OSLog for BLE/MQTT/Background to verify restoration/reconnect flows in Console and correlate with broker timestamps.
Current Behavior
- After the app remains in background for ~5–30+ minutes (sometimes longer), BLE→MQTT forwarding stops; broker topics are quiet.
- Bringing the app to the foreground immediately resumes forwarding.
- This reproduces consistently with:
- Background App Refresh: ON
- Low Power Mode: OFF
- Location: Always Allow
- App not force‑quit (remains in switcher)
- Broker logs show a gap matching the background interval, ending at the moment of foreground.
Evidence in code (why this likely happens)
- No CoreBluetooth state restoration
// CBCentralManager created without a restoration identifier
centralManager = CBCentralManager(
delegate: delegate,
queue: .global(qos: .utility)
)
// Restoration delegate exists but commented/unwired:
// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { ... }
- MQTT session tied to process lifetime
// Typical setup
mqtt.autoReconnect = true
mqtt.keepAlive = 60
mqtt.cleanSession = true // ❌ loses subs across reconnects
// -> Once iOS suspends the process, reconnects don't happen until foreground.
- Wake sources not used to re‑attach
- App has location background capability; not used to nudge BLE/MQTT.
- No BGTaskScheduler to opportunistically refresh in background.
Proposed fix sketch (minimal, opt‑in)
CoreBluetooth restoration
centralManager = CBCentralManager(
delegate: delegate,
queue: .global(qos: .utility),
options: [CBCentralManagerOptionRestoreIdentifierKey: "com.meshtastic.central"]
)
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
// reconnect known peripherals or resume scan
BackgroundGateway.shared.resumeIfNeeded()
}
MQTT resilience
mqtt.cleanSession = false
mqtt.keepAlive = 30
func reconnectIfNeeded() {
guard mqtt.connState != .connected && mqtt.connState != .connecting else { return }
mqtt.connect()
}
Background nudges
// BGTaskScheduler + location updates call:
BackgroundGateway.shared.reconnectIfNeeded()
Info.plist
- Add
fetch to UIBackgroundModes
- Add
com.meshtastic.gateway.refresh to BGTaskSchedulerPermittedIdentifiers
Logging
- OSLog categories for BLE/MQTT/Background; verify restoration/reconnects against broker timestamps.
Participation
Additional comments
No response
Code of Conduct
Before submitting
Firmware Version
2.7.1
What did you do?
Environment
Steps
Observed outcome
Technical context / code pointers (what I looked at)
1) CoreBluetooth central has no state restoration
willRestoreState.2) MQTT client is process-lifetime bound
autoReconnectonly helps while the process is active; iOS suspends background apps.cleanSession = true⇒ broker discards subscriptions between connects.keepAlive = 60⇒ stale links detected slowly in background mode.3) Background wake sources not leveraged to re‑attach BLE/MQTT
Proposed fix (surgical and opt‑in)
A) Enable CoreBluetooth state preservation & restoration
B) Make MQTT more resilient in background
C) Opportunistic wake handlers (BLE restore, location, BG refresh)
D) Info.plist additions
UIBackgroundModes: add bluetooth-central, location, fetchBGTaskSchedulerPermittedIdentifiers:["com.meshtastic.gateway.refresh"]E) Logging
Why opt‑in? Battery/OS‑throttling tradeoffs; keep default behavior unchanged for most users.
Expected Behavior
cleanSession = false(retain subs across reconnects)keepAlive ≈ 30s(faster stale detection in bg)Acceptance (practical)
Code‑level expectations (what the app should do)
CoreBluetooth restoration path
MQTT reconnection path on wakes
Info.plist / background config
UIBackgroundModes:bluetooth-central,location,fetchBGTaskSchedulerPermittedIdentifiers:com.meshtastic.gateway.refreshLogging
Current Behavior
Evidence in code (why this likely happens)
Proposed fix sketch (minimal, opt‑in)
CoreBluetooth restoration
MQTT resilience
Background nudges
Info.plist
fetchtoUIBackgroundModescom.meshtastic.gateway.refreshtoBGTaskSchedulerPermittedIdentifiersLogging
Participation
Additional comments
No response
Code of Conduct