@@ -123,6 +123,11 @@ class EquilBLE @Inject constructor(
123123 isConnected = true
124124 equilManager.equilState?.bluetoothConnectionState = BluetoothConnectionState .CONNECTED
125125 handler.removeMessages(TIME_OUT_CONNECT_WHAT )
126+ synchronized(notifyLock) {
127+ // New link: notifications not yet enabled. Block command dispatch until onDescriptorWrite.
128+ notificationEnabled = false
129+ pendingCmd = null
130+ }
126131 bluetoothGatt?.discoverServices()
127132 updateCmdStatus(ResolvedResult .FAILURE )
128133 // rxBus.send(new EventPumpStatusChanged(EventPumpStatusChanged.Status.CONNECTED));
@@ -175,7 +180,14 @@ class EquilBLE @Inject constructor(
175180 aapsLogger.debug(LTag .PUMPBTCOMM , " onDescriptorWrite received: $status " )
176181 if (status == BluetoothGatt .GATT_SUCCESS ) {
177182 aapsLogger.debug(LTag .PUMPBTCOMM , " onDescriptorWrite: Wrote GATT Descriptor successfully." )
178- ready()
183+ synchronized(notifyLock) {
184+ notificationEnabled = true
185+ // Flush a command that writeCmd deferred while notifications were coming up.
186+ if (pendingCmd != null ) {
187+ pendingCmd = null
188+ ready()
189+ }
190+ }
179191 }
180192 }
181193 }
@@ -236,6 +248,10 @@ class EquilBLE @Inject constructor(
236248 bluetoothGatt = null
237249 baseCmd = null
238250 preCmd = null
251+ synchronized(notifyLock) {
252+ notificationEnabled = false
253+ pendingCmd = null
254+ }
239255 rxBus.send(EventPumpStatusChanged (EventPumpStatusChanged .Status .DISCONNECTED ))
240256 }
241257
@@ -267,6 +283,21 @@ class EquilBLE @Inject constructor(
267283
268284 private var baseCmd: BaseCmd ? = null
269285 private var preCmd: BaseCmd ? = null
286+
287+ // Notification-readiness gate for the current GATT connection. Android allows only ONE outstanding
288+ // GATT operation at a time. When the queue's connect() phase opens the link, `isConnected` flips
289+ // true at onConnectionStateChange(CONNECTED) - BEFORE onServicesDiscovered runs openNotification()
290+ // (the notify-descriptor write). If a command's writeCmd then writes its first characteristic packet
291+ // in that window, it collides with the pending descriptor write: writeDescriptor() returns false
292+ // (log: "openNotification: false"), notifications never enable, the pump's replies never arrive, and
293+ // the command idle-times-out after ~9 s -> "Pump connection failure / manually check delivered
294+ // insulin" (bolus, tempBasal, and profile/CmdSettingSet all hit this via different writeCmd branches).
295+ // 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).
297+ private val notifyLock = Any ()
298+ @Volatile private var notificationEnabled = false
299+ private var pendingCmd: BaseCmd ? = null
300+
270301 fun writeCmd (baseCmd : BaseCmd ) {
271302 aapsLogger.debug(LTag .PUMPCOMM , " writeCmd {}" , baseCmd)
272303 this .baseCmd = baseCmd
@@ -276,6 +307,18 @@ class EquilBLE @Inject constructor(
276307 else -> equilManager?.equilState?.address ? : error(" Unknown MAC address" )
277308 }
278309 autoScan = baseCmd is CmdRunningModeGet || baseCmd is CmdInsulinGet
310+ if (isConnected) {
311+ synchronized(notifyLock) {
312+ 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
317+ preCmd = baseCmd
318+ return
319+ }
320+ }
321+ }
279322 if (isConnected && baseCmd.isPairStep()) {
280323 ready()
281324 } else if (isConnected) {
@@ -285,11 +328,8 @@ class EquilBLE @Inject constructor(
285328 baseCmd.runPwd = prevCmd.runPwd
286329 nextCmd2()
287330 } else {
288- // The GATT link was opened by the queue's connect() phase, which leaves no prior
289- // command context (baseCmd/preCmd are null, so the connect-time ready() was a no-op).
290- // Send this command as the first one on the open connection instead of silently
291- // dropping it - otherwise the pump receives nothing and idle-disconnects (status 19),
292- // surfacing as a bolus/command timeout. See issue #4910.
331+ // GATT link opened by the queue's connect() phase, notifications already up: send this
332+ // command as the first one on the open link (else the pump idle-disconnects, status 19).
293333 ready()
294334 }
295335 } else {
0 commit comments