@@ -18,7 +18,6 @@ import android.content.Context
1818import android.content.pm.PackageManager
1919import android.os.Handler
2020import android.os.HandlerThread
21- import android.os.Looper
2221import android.os.Message
2322import android.os.SystemClock
2423import android.text.TextUtils
@@ -39,9 +38,7 @@ import app.aaps.pump.equil.manager.Utils
3938import app.aaps.pump.equil.manager.command.BaseCmd
4039import app.aaps.pump.equil.manager.command.CmdDevicesOldGet
4140import app.aaps.pump.equil.manager.command.CmdHistoryGet
42- import app.aaps.pump.equil.manager.command.CmdInsulinGet
4341import app.aaps.pump.equil.manager.command.CmdPair
44- import app.aaps.pump.equil.manager.command.CmdRunningModeGet
4542import java.util.UUID
4643import javax.inject.Inject
4744import javax.inject.Singleton
@@ -123,10 +120,13 @@ class EquilBLE @Inject constructor(
123120 isConnected = true
124121 equilManager.equilState?.bluetoothConnectionState = BluetoothConnectionState .CONNECTED
125122 handler.removeMessages(TIME_OUT_CONNECT_WHAT )
123+ // Link is up: stop the parallel advert-harvest scan (the pump stops advertising once
124+ // connected anyway). If it already caught an advert it stopped itself in onScanResult.
125+ stopScan()
126126 synchronized(notifyLock) {
127127 // New link: notifications not yet enabled. Block command dispatch until onDescriptorWrite.
128128 notificationEnabled = false
129- pendingCmd = null
129+ dispatchedCmd = null
130130 }
131131 bluetoothGatt?.discoverServices()
132132 updateCmdStatus(ResolvedResult .FAILURE )
@@ -182,11 +182,9 @@ class EquilBLE @Inject constructor(
182182 aapsLogger.debug(LTag .PUMPBTCOMM , " onDescriptorWrite: Wrote GATT Descriptor successfully." )
183183 synchronized(notifyLock) {
184184 notificationEnabled = true
185- // Flush a command that writeCmd deferred while notifications were coming up.
186- if (pendingCmd != null ) {
187- pendingCmd = null
188- ready()
189- }
185+ // Notifications live: send the command (queue-opened, deferred, or issued while the
186+ // link was down). Send-once via dispatchedCmd so it can't collide/double with writeCmd.
187+ dispatchCmd()
190188 }
191189 }
192190 }
@@ -237,9 +235,11 @@ class EquilBLE @Inject constructor(
237235 }
238236
239237 fun disconnect () {
238+ stopScan() // stop any in-flight advert-harvest scan (hybrid connect)
240239 isConnected = false
241240 connecting = false
242241 startTrue = false
242+ connectInitiated = false
243243 autoScan = false
244244 equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState .DISCONNECTED
245245 aapsLogger.debug(LTag .PUMPBTCOMM , " Closing GATT connection" )
@@ -250,7 +250,7 @@ class EquilBLE @Inject constructor(
250250 preCmd = null
251251 synchronized(notifyLock) {
252252 notificationEnabled = false
253- pendingCmd = null
253+ dispatchedCmd = null
254254 }
255255 rxBus.send(EventPumpStatusChanged (EventPumpStatusChanged .Status .DISCONNECTED ))
256256 }
@@ -267,16 +267,21 @@ class EquilBLE @Inject constructor(
267267 private fun findEquil (mac : String ) {
268268 if (mac.isEmpty()) return
269269 if (isConnected) return
270- val equilDevice: BluetoothDevice ? = bluetoothAdapter?.getRemoteDevice(mac)
271- if (autoScan) startScan()
272- else connectEquil(equilDevice)
270+ // Known pump: connect straight to the MAC (autoConnect), no scan. See connect() / #5040.
271+ // Mirror connect()'s state handling so isConnecting() reflects the in-flight direct connect.
272+ connecting = true
273+ equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState .CONNECTING
274+ connectEquil(bluetoothAdapter?.getRemoteDevice(mac))
273275 }
274276
275277 fun connectEquil (device : BluetoothDevice ? ) {
276278 handler.postDelayed({
277279 if (device != null ) {
278280 aapsLogger.debug(LTag .PUMPCOMM , " connectEquil======" )
279- bluetoothGatt = device.connectGatt(context, false , mGattCallback, BluetoothDevice .TRANSPORT_LE )
281+ // autoConnect = true: the Android stack completes the link as soon as the (known/bonded)
282+ // pump is in range, with no app-level scan. This replaces flaky scan discovery, which took
283+ // 60-90 s on many phones and caused command timeouts / "no insulin delivered" (#5040).
284+ bluetoothGatt = device.connectGatt(context, true , mGattCallback, BluetoothDevice .TRANSPORT_LE )
280285 }
281286 }, 500 )
282287 }
@@ -293,34 +298,44 @@ class EquilBLE @Inject constructor(
293298 // the command idle-times-out after ~9 s -> "Pump connection failure / manually check delivered
294299 // insulin" (bolus, tempBasal, and profile/CmdSettingSet all hit this via different writeCmd branches).
295300 // Fix: never send on a connected link until onDescriptorWrite confirms notifications are enabled;
296- // hold the command in `pendingCmd` and let onDescriptorWrite flush it. See #4910 (and its follow-up).
301+ // dispatchedCmd makes that send-once (per link) so writeCmd and onDescriptorWrite can't double-send, and
302+ // the command dispatches after connect regardless of which writeCmd branch opened the link - including a
303+ // command issued while disconnected (the reservoir-change case, where the pump drops BLE between steps).
304+ // Ported from the dev branch. See #4910 / #5040.
297305 private val notifyLock = Any ()
298306 @Volatile private var notificationEnabled = false
299- private var pendingCmd: BaseCmd ? = null
307+ private var dispatchedCmd: BaseCmd ? = null
308+
309+ // Send the current command's first packet exactly once per link, only after notifications are enabled.
310+ // Null-safe (no-op during the pure connect handshake). Caller MUST hold notifyLock.
311+ private fun dispatchCmd () {
312+ val cmd = baseCmd
313+ if (cmd != null && cmd != = dispatchedCmd) {
314+ dispatchedCmd = cmd
315+ ready()
316+ }
317+ }
300318
301319 fun writeCmd (baseCmd : BaseCmd ) {
302320 aapsLogger.debug(LTag .PUMPCOMM , " writeCmd {}" , baseCmd)
303321 this .baseCmd = baseCmd
304322 val mac: String = when (baseCmd) {
305- is CmdPair -> baseCmd.address
323+ is CmdPair -> baseCmd.address
306324 is CmdDevicesOldGet -> baseCmd.address
307- else -> equilManager?.equilState?.address ? : error(" Unknown MAC address" )
325+ else -> equilManager?.equilState?.address ? : error(" Unknown MAC address" )
308326 }
309- autoScan = baseCmd is CmdRunningModeGet || baseCmd is CmdInsulinGet
310327 if (isConnected) {
311328 synchronized(notifyLock) {
312329 if (! notificationEnabled) {
313- // Fresh link, notifications not enabled yet: defer ALL send paths (pair step,
314- // continuation, or first command) so the characteristic write does not collide with
315- // the openNotification() descriptor write. onDescriptorWrite flushes pendingCmd.
316- pendingCmd = baseCmd
330+ // Fresh link, notifications not enabled yet: defer ALL send paths. onDescriptorWrite ->
331+ // dispatchCmd() sends the command once notifications are up (no descriptor collision).
317332 preCmd = baseCmd
318333 return
319334 }
320335 }
321336 }
322337 if (isConnected && baseCmd.isPairStep()) {
323- ready()
338+ synchronized(notifyLock) { dispatchCmd() }
324339 } else if (isConnected) {
325340 val prevCmd = preCmd
326341 if (prevCmd != null ) {
@@ -330,7 +345,7 @@ class EquilBLE @Inject constructor(
330345 } else {
331346 // GATT link opened by the queue's connect() phase, notifications already up: send this
332347 // command as the first one on the open link (else the pump idle-disconnects, status 19).
333- ready()
348+ synchronized(notifyLock) { dispatchCmd() }
334349 }
335350 } else {
336351 findEquil(mac)
@@ -348,7 +363,7 @@ class EquilBLE @Inject constructor(
348363 preCmd = baseCmd
349364 } else {
350365 aapsLogger.debug(LTag .PUMPCOMM , " readHistory error" )
351- synchronized(baseCmd) { (baseCmd as Object ).notifyAll() }
366+ synchronized(baseCmd) { (baseCmd as Any ).notifyAll() }
352367 }
353368 }
354369
@@ -381,6 +396,7 @@ class EquilBLE @Inject constructor(
381396 }
382397
383398 private var dataList: List <String > = ArrayList ()
399+
384400 @Synchronized
385401 fun decode (buffer : ByteArray ) {
386402 val str = Utils .bytesToHex(buffer)
@@ -407,7 +423,7 @@ class EquilBLE @Inject constructor(
407423 override fun handleMessage (msg : Message ) {
408424 super .handleMessage(msg)
409425 when (msg.what) {
410- TIME_OUT_WHAT -> stopScan()
426+ TIME_OUT_WHAT -> stopScan()
411427
412428 TIME_OUT_CONNECT_WHAT -> {
413429 stopScan()
@@ -420,12 +436,18 @@ class EquilBLE @Inject constructor(
420436 }
421437 }
422438 private var startTrue = false
439+
440+ // One-shot guard: set true when onScanResult fires the connect for the current scan session, re-armed at
441+ // each startScan(). Prevents the rapid LOW_LATENCY result stream from opening multiple GATT clients.
442+ private var connectInitiated = false
443+
423444 private fun startScan () {
424445 macAddress = equilManager?.equilState?.address
425446 aapsLogger.debug(LTag .PUMPBTCOMM , " startScan====$startTrue ====$macAddress ===" )
426447 if (macAddress.isNullOrEmpty()) return
427448 if (startTrue) return
428449 startTrue = true
450+ connectInitiated = false
429451 connecting = true
430452 equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState .CONNECTING
431453 if (ActivityCompat .checkSelfPermission(context, Manifest .permission.BLUETOOTH_SCAN ) == PackageManager .PERMISSION_GRANTED ) {
@@ -451,9 +473,26 @@ class EquilBLE @Inject constructor(
451473 if (connecting || isConnected) {
452474 return
453475 }
454- autoScan = true
455476 baseCmd = null
456- startScan()
477+ macAddress = equilManager?.equilState?.address
478+ val device = macAddress?.takeIf { it.isNotEmpty() }?.let { bluetoothAdapter?.getRemoteDevice(it) }
479+ if (device != null ) {
480+ // Known/bonded pump: connect straight to its MAC (see connectEquil, autoConnect=true) instead
481+ // of scanning-to-connect. Scan-to-connect was the #5040 bottleneck (60-90 s on many phones).
482+ connecting = true
483+ equilManager?.equilState?.bluetoothConnectionState = BluetoothConnectionState .CONNECTING
484+ connectEquil(device)
485+ // Hybrid: run a best-effort advertisement harvest IN PARALLEL. It does NOT gate the connection
486+ // (autoConnect above owns that), but the advert carries data the GATT path can't get: the pump's
487+ // current history index (needed so loadEquilHistory reads new records), battery/reservoir, and the
488+ // live alarm state. autoScan=false so onScanResult only decodes the advert - it does not open a
489+ // second GATT client. The scan is stopped on CONNECTED / onScanResult / disconnect. See #5040.
490+ autoScan = false
491+ startScan()
492+ } else {
493+ autoScan = true
494+ startScan()
495+ }
457496 }
458497
459498 private fun buildScanFilters (): List <ScanFilter > {
@@ -469,27 +508,36 @@ class EquilBLE @Inject constructor(
469508
470509 private fun buildScanSettings (): ScanSettings {
471510 val builder = ScanSettings .Builder ()
511+ // Command connects are latency-sensitive (a bolus/temp-basal is waiting on discovery). The default
512+ // SCAN_MODE_LOW_POWER duty-cycles the radio and can take tens of seconds to surface a bonded pump on
513+ // some phones, long enough for the command to time out. Use LOW_LATENCY so the pump is found in ~1 s.
514+ builder.setScanMode(ScanSettings .SCAN_MODE_LOW_LATENCY )
472515 builder.setReportDelay(0 )
473516 return builder.build()
474517 }
475518
476519 private var scanCallback: ScanCallback = object : ScanCallback () {
477520 override fun onScanResult (callbackType : Int , result : ScanResult ) {
478521 super .onScanResult(callbackType, result)
479- val name: String? = result.device.name
480- if (name?.isNotEmpty() == true ) {
481- try {
522+ // The scan is filtered by MAC address (buildScanFilters), so every result IS the target pump.
523+ // Do NOT gate on result.device.name: it is frequently null until the OS caches the device name,
524+ // which silently drops valid matches and stalls discovery for tens of seconds. Guard with a
525+ // one-shot flag so the rapid LOW_LATENCY result stream opens only a single GATT client.
526+ if (connectInitiated) return
527+ connectInitiated = true
528+ try {
529+ result.scanRecord?.bytes?.let { bytes ->
482530 bleHandler.post {
483- equilManager?.decodeData(result.scanRecord!! .bytes, autoScan)
484- }
485- stopScan()
486- if (autoScan) {
487- updateCmdStatus(ResolvedResult .CONNECT_ERROR )
488- connectEquil(result.device)
531+ equilManager?.decodeData(bytes, autoScan)
489532 }
490- } catch (e: Exception ) {
491- e.printStackTrace()
492533 }
534+ stopScan()
535+ if (autoScan) {
536+ updateCmdStatus(ResolvedResult .CONNECT_ERROR )
537+ connectEquil(result.device)
538+ }
539+ } catch (e: Exception ) {
540+ aapsLogger.error(LTag .PUMPBTCOMM , " onScanResult error" , e)
493541 }
494542 }
495543 }
0 commit comments