Skip to content

Commit 598e2eb

Browse files
authored
Merge pull request #5046 from nightscout/dev3
3.4.2.6
2 parents 8456c9d + 335465c commit 598e2eb

4 files changed

Lines changed: 104 additions & 57 deletions

File tree

.github/workflows/aaps-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ on:
66
tagName:
77
description: 'Select AAPS Version'
88
required: true
9-
default: '3.4.2.5'
9+
default: '3.4.2.6'
1010
type: choice
1111
options:
1212
# ── 3.4.x (JDK 21) ──────────────────────────────
13-
- 3.4.2.5
13+
- 3.4.2.6
1414
- 3.4.2.4
1515
- 3.4.2.3
1616
- 3.4.2.2

buildSrc/src/main/kotlin/Versions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
55
object Versions {
66

77
// On change edit aaps-ci.yml
8-
const val appVersion = "3.4.2.5"
8+
const val appVersion = "3.4.2.6"
99
const val versionCode = 1500
1010

1111
const val compileSdk = 36

pump/equil/src/main/kotlin/app/aaps/pump/equil/EquilPumpPlugin.kt

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,19 @@ class EquilPumpPlugin @Inject constructor(
116116
.toObservable(EventEquilAlarm::class.java)
117117
.observeOn(aapsSchedulers.io)
118118
.subscribe({ eventEquilError ->
119-
commandQueue.performing()?.let {
120-
if (it.commandType == Command.CommandType.BOLUS) {
121-
aapsLogger.info(
122-
LTag.PUMPCOMM,
123-
"eventEquilError.tips====${eventEquilError.tips}"
124-
)
125-
rxBus.send(EventDismissNotification(Notification.EQUIL_ALARM))
126-
equilManager.showNotification(
127-
Notification.EQUIL_ALARM,
128-
eventEquilError.tips,
129-
Notification.URGENT, app.aaps.core.ui.R.raw.alarm
130-
)
131-
stopBolusDelivering()
132-
}
119+
aapsLogger.info(LTag.PUMPCOMM, "eventEquilError.tips====${eventEquilError.tips}")
120+
// Always surface the pump alarm - it is no longer gated on a bolus being in
121+
// progress (alarms now come from the GATT history read on every connection, not
122+
// just from an advertisement scan caught mid-bolus). See #5040.
123+
rxBus.send(EventDismissNotification(Notification.EQUIL_ALARM))
124+
equilManager.showNotification(
125+
Notification.EQUIL_ALARM,
126+
eventEquilError.tips,
127+
Notification.URGENT, app.aaps.core.ui.R.raw.alarm
128+
)
129+
// But only halt bolus tracking if a bolus is actually delivering.
130+
if (commandQueue.performing()?.commandType == Command.CommandType.BOLUS) {
131+
stopBolusDelivering()
133132
}
134133
}, fabricPrivacy::logException)
135134

pump/equil/src/main/kotlin/app/aaps/pump/equil/ble/EquilBLE.kt

Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import android.content.Context
1818
import android.content.pm.PackageManager
1919
import android.os.Handler
2020
import android.os.HandlerThread
21-
import android.os.Looper
2221
import android.os.Message
2322
import android.os.SystemClock
2423
import android.text.TextUtils
@@ -39,9 +38,7 @@ import app.aaps.pump.equil.manager.Utils
3938
import app.aaps.pump.equil.manager.command.BaseCmd
4039
import app.aaps.pump.equil.manager.command.CmdDevicesOldGet
4140
import app.aaps.pump.equil.manager.command.CmdHistoryGet
42-
import app.aaps.pump.equil.manager.command.CmdInsulinGet
4341
import app.aaps.pump.equil.manager.command.CmdPair
44-
import app.aaps.pump.equil.manager.command.CmdRunningModeGet
4542
import java.util.UUID
4643
import javax.inject.Inject
4744
import 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

Comments
 (0)