@@ -51,6 +51,7 @@ class EversenseGattCallback(
5151 private const val magicDescriptorUUID = " 00002902-0000-1000-8000-00805f9b34fb"
5252
5353 private const val WRITE_TIMEOUT_MS = 5000L
54+ private const val CALIBRATION_TIMEOUT_MS = 15000L
5455 }
5556
5657 // FIX 1: Dedicated BLE executor for callbacks; separate network executor for HTTP calls
@@ -77,19 +78,17 @@ class EversenseGattCallback(
7778 @Volatile
7879 private var connected: Boolean = false
7980
80- // Tracks consecutive failed connection attempts to detect transmitter placement issues
81+ // Tracks consecutive status-19 failures to detect transmitter placement issues
8182 @Volatile
8283 private var failedConnectionAttempts: Int = 0
8384 private val PLACEMENT_WARNING_THRESHOLD = 3
8485
85- // When true, AAPS disconnects after each sync to give the official Eversense app
86- // a 2-minute window to connect and upload data to the cloud. Reconnects automatically.
86+ // Tracks consecutive general reconnect attempts (reset on successful connection).
87+ // Used to compute exponential backoff so AAPS retries quickly after boot (when the
88+ // official Eversense app temporarily holds the BLE connection) and backs off for
89+ // sustained failures to avoid draining the battery.
8790 @Volatile
88- var coexistenceMode: Boolean = false
89-
90- // True during a planned coexistence disconnect — suppresses normal auto-reconnect
91- @Volatile
92- private var plannedDisconnect: Boolean = false
91+ private var reconnectAttempts: Int = 0
9392
9493 fun isConnected (): Boolean = connected
9594 fun is365 (): Boolean = security == EversenseSecurityType .SecureV2
@@ -135,6 +134,9 @@ class EversenseGattCallback(
135134 bluetoothGatt = gatt
136135 // FIX 3: Set connected flag on confirmed STATE_CONNECTED.
137136 connected = true
137+ // Reset backoff counters on successful connection
138+ reconnectAttempts = 0
139+ failedConnectionAttempts = 0
138140
139141 preferences.edit(commit = true ) {
140142 putString(StorageKeys .REMOTE_DEVICE_KEY , gatt.device.address)
@@ -147,7 +149,9 @@ class EversenseGattCallback(
147149 }
148150
149151 if (! gatt.requestMtu(512 )) {
150- EversenseLogger .error(TAG , " Failed to request MTU" )
152+ EversenseLogger .warning(TAG , " requestMtu returned false — skipping to discoverServices with default payload size" )
153+ payloadSize = 20
154+ gatt.discoverServices()
151155 }
152156 return
153157 }
@@ -173,15 +177,33 @@ class EversenseGattCallback(
173177 }
174178
175179 val storedAddress = preferences.getString(StorageKeys .REMOTE_DEVICE_KEY , null )
176- if (storedAddress != null && ! plannedDisconnect) {
177- val delayMs = if (status == BluetoothGatt .GATT_SUCCESS ) 5000L else 10000L
178- EversenseLogger .info(TAG , " Scheduling auto-reconnect in ${delayMs/ 1000 } s (status: $status )" )
180+ if (storedAddress != null ) {
181+ // Exponential backoff so AAPS reclaims the transmitter quickly after boot
182+ // (when the official Eversense app temporarily holds the BLE connection)
183+ // and avoids battery drain during sustained unavailability.
184+ //
185+ // Status 19 = transmitter actively rejected us (placement issue, not competition) —
186+ // use a fixed 30 s interval so we don't spam it.
187+ // Status GATT_SUCCESS = clean disconnect (we or the transmitter closed cleanly) —
188+ // reconnect quickly in 5 s.
189+ // All other status codes (e.g. 133 = GATT_ERROR, device busy) = backoff:
190+ // attempt 0 → 5 s, attempt 1 → 10 s, attempt 2 → 20 s, attempt 3 → 40 s,
191+ // attempt 4+ → 60 s cap.
192+ val delayMs: Long = when {
193+ status == 19 -> 30_000L
194+ status == BluetoothGatt .GATT_SUCCESS -> 5_000L
195+ else -> {
196+ val attempt = reconnectAttempts++
197+ minOf(5_000L * (1L shl minOf(attempt, 4 )), 60_000L )
198+ }
199+ }
200+ EversenseLogger .info(TAG , " Scheduling auto-reconnect in ${delayMs / 1000 } s (status: $status , attempt: $reconnectAttempts )" )
179201 handler.postDelayed({
180- EversenseLogger .info(TAG , " Attempting auto-reconnect..." )
202+ EversenseLogger .info(TAG , " Attempting auto-reconnect (attempt $reconnectAttempts ) ..." )
181203 plugin.connect(null )
182204 }, delayMs)
183205 } else {
184- if ( ! plannedDisconnect) { EversenseLogger .warning(TAG , " No stored device address — skipping auto-reconnect" ) } else { EversenseLogger .info( TAG , " Planned coexistence disconnect — suppressing auto-reconnect " ) }
206+ EversenseLogger .warning(TAG , " No stored device address — skipping auto-reconnect" )
185207 }
186208 }
187209 }
@@ -318,16 +340,6 @@ class EversenseGattCallback(
318340 bleExecutor.submit {
319341 EversenseE3Communicator .readGlucose(this , preferences, plugin.watchers)
320342 EversenseE3Communicator .fullSync(this , preferences, plugin.watchers)
321- if (coexistenceMode) {
322- EversenseLogger .info(TAG , " Coexistence — disconnecting to give official app 2-minute window" )
323- plannedDisconnect = true
324- disconnect()
325- handler.postDelayed({
326- EversenseLogger .info(TAG , " Coexistence reconnect — official app window ended" )
327- plannedDisconnect = false
328- plugin.connect(null )
329- }, 120000L )
330- }
331343 }
332344 return
333345 }
@@ -345,16 +357,6 @@ class EversenseGattCallback(
345357 Eversense365Communicator .readGlucose(this , preferences, plugin.watchers)
346358 Eversense365Communicator .fullSync(this , preferences, plugin.watchers)
347359 }
348- if (coexistenceMode && ! plannedDisconnect) {
349- EversenseLogger .info(TAG , " Coexistence — disconnecting to give official app 2-minute window" )
350- plannedDisconnect = true
351- disconnect()
352- handler.postDelayed({
353- EversenseLogger .info(TAG , " Coexistence reconnect — official app window ended" )
354- plannedDisconnect = false
355- plugin.connect(null )
356- }, 120000L )
357- }
358360 }
359361 return
360362 } else if (data.size >= 4 && data[0 ] == Eversense365Packets .NotificationResponseId && data[1 ] == 0x03 .toByte()) {
@@ -389,12 +391,13 @@ class EversenseGattCallback(
389391
390392 if (EversenseE3Packets .isErrorPacket(data[0 ])) {
391393 EversenseLogger .error(TAG , " Received error response - data: ${data.toHexString()} " )
394+ packet.isErrorResponse = true
392395 packet.notifyAll()
393396 return
394397 }
395398
396399 if (security == EversenseSecurityType .None ) {
397- if (packetAnnotation.responseId != data[0 ]) {
400+ if (! packet.skipResponseIdValidation && packetAnnotation.responseId != data[0 ]) {
398401 EversenseLogger .warning(TAG , " Incorrect responseId - expected: ${packetAnnotation.responseId} , got: ${data[0 ]} " )
399402 return
400403 }
@@ -419,7 +422,7 @@ class EversenseGattCallback(
419422 @SuppressLint(" MissingPermission" )
420423 @OptIn(ExperimentalStdlibApi ::class )
421424 @Throws(EversenseWriteException ::class )
422- fun <T : EversenseBasePacket .Response > writePacket (packet : EversenseBasePacket ): T {
425+ fun <T : EversenseBasePacket .Response > writePacket (packet : EversenseBasePacket , timeoutMs : Long = WRITE_TIMEOUT_MS ): T {
423426 val gatt = bluetoothGatt ? : throw EversenseWriteException (" Gatt is null — not connected" )
424427
425428 val requestCharacteristic = requestCharacteristic
@@ -441,11 +444,14 @@ class EversenseGattCallback(
441444 // Previously, a timeout would fall through to parseResponse() silently, likely
442445 // producing a confusing cast exception rather than a clear timeout error.
443446 val start = System .currentTimeMillis()
444- packet.wait(WRITE_TIMEOUT_MS )
447+ packet.wait(timeoutMs )
445448 val elapsed = System .currentTimeMillis() - start
446- if (elapsed >= WRITE_TIMEOUT_MS ) {
449+ if (elapsed >= timeoutMs ) {
447450 currentPacket.set(null )
448- throw EversenseWriteException (" Timed out waiting for response after ${WRITE_TIMEOUT_MS } ms — packet: ${packet.getAnnotation()?.responseId} " )
451+ throw EversenseWriteException (" Timed out waiting for response after ${timeoutMs} ms — packet: ${packet.getAnnotation()?.responseId} " )
452+ } else if (packet.isErrorResponse) {
453+ currentPacket.set(null )
454+ throw EversenseWriteException (" Transmitter returned error response — packet: ${packet.getAnnotation()?.responseId} " )
449455 }
450456 } catch (e: EversenseWriteException ) {
451457 throw e
@@ -478,6 +484,8 @@ class EversenseGattCallback(
478484
479485 EversenseLogger .info(TAG , " E3 auth complete — ready for full sync" )
480486 EversenseE3Communicator .fullSync(this , preferences, plugin.watchers)
487+ EversenseLogger .info(TAG , " E3 transmitter ready — notifying watchers" )
488+ handler.post { plugin.watchers.forEach { it.onTransmitterReady() } }
481489 }
482490
483491 @SuppressLint(" MissingPermission" )
@@ -507,6 +515,11 @@ class EversenseGattCallback(
507515 return
508516 }
509517
518+ // Cache access token so it can be used for cloud uploads without re-login
519+ val expiryMs = System .currentTimeMillis() + (authSession.expires_in * 1000L )
520+ preferences.edit().putString(StorageKeys .ACCESS_TOKEN , authSession.access_token)
521+ .putLong(StorageKeys .ACCESS_TOKEN_EXPIRY , expiryMs).apply ()
522+
510523 val fleet = networkExecutor.submit<Any ?> {
511524 EversenseHttp365Util .getFleetSecretV2(
512525 accessToken = authSession.access_token,
@@ -543,6 +556,8 @@ class EversenseGattCallback(
543556
544557 EversenseLogger .info(TAG , " 365 auth complete — ready for full sync" )
545558 Eversense365Communicator .fullSync(this , preferences, plugin.watchers)
559+ EversenseLogger .info(TAG , " 365 transmitter ready — notifying watchers" )
560+ handler.post { plugin.watchers.forEach { it.onTransmitterReady() } }
546561
547562 } catch (exception: Exception ) {
548563 EversenseLogger .error(TAG , " [365] authV2 failed: $exception " )
0 commit comments