Compare commits

...

2 Commits

Author SHA1 Message Date
alex 15d13cc52f Recover Android BLE connection after Pi restart 2026-08-01 01:08:55 +02:00
alex d2934690a5 Fix BLE advertising after reboot 2026-08-01 01:08:48 +02:00
3 changed files with 42 additions and 8 deletions
@@ -12,6 +12,7 @@ import android.os.ParcelUuid
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.json.JSONObject import org.json.JSONObject
@@ -36,19 +37,35 @@ class BleClient(private val context: Context) {
private val writes = ArrayDeque<ByteArray>() private val writes = ArrayDeque<ByteArray>()
private var writeInProgress = false private var writeInProgress = false
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val reconnect: Runnable = Runnable { connect() }
private val restartScan: Runnable = Runnable {
if (!stopped && state.value == ConnectionState.SCANNING) {
bluetoothManager.adapter?.bluetoothLeScanner?.stopScan(scanCallback)
scheduleReconnect(1_000)
}
}
private var stopped = true private var stopped = true
fun connect() { fun connect() {
stopped = false stopped = false
val adapter = bluetoothManager.adapter ?: return handler.removeCallbacks(reconnect)
handler.removeCallbacks(restartScan)
val adapter = bluetoothManager.adapter ?: run {
state.value = ConnectionState.DISCONNECTED
scheduleReconnect()
return
}
adapter.bluetoothLeScanner?.stopScan(scanCallback)
state.value = ConnectionState.SCANNING state.value = ConnectionState.SCANNING
val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build() val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build()
adapter.bluetoothLeScanner?.startScan(listOf(filter), ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), scanCallback) adapter.bluetoothLeScanner?.startScan(listOf(filter), ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), scanCallback)
handler.postDelayed(restartScan, 15_000)
} }
fun disconnect() { fun disconnect() {
stopped = true stopped = true
handler.removeCallbacksAndMessages(null) handler.removeCallbacks(reconnect)
handler.removeCallbacks(restartScan)
bluetoothManager.adapter?.bluetoothLeScanner?.stopScan(scanCallback) bluetoothManager.adapter?.bluetoothLeScanner?.stopScan(scanCallback)
gatt?.close(); gatt = null; rx = null gatt?.close(); gatt = null; rx = null
resetWrites() resetWrites()
@@ -64,21 +81,30 @@ class BleClient(private val context: Context) {
enqueue((message.toString() + "\n").toByteArray()) enqueue((message.toString() + "\n").toByteArray())
return try { return try {
withTimeout(20_000) { deferred.await() } withTimeout(20_000) { deferred.await() }
} catch (error: TimeoutCancellationException) {
handler.post { gatt?.disconnect() }
throw error
} finally { } finally {
pending.remove(id) pending.remove(id)
} }
} }
private val scanCallback = object : ScanCallback() { private val scanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) { override fun onScanResult(callbackType: Int, result: ScanResult) {
handler.removeCallbacks(restartScan)
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(this) bluetoothManager.adapter.bluetoothLeScanner?.stopScan(this)
state.value = ConnectionState.CONNECTING state.value = ConnectionState.CONNECTING
gatt?.close()
gatt = result.device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) gatt = result.device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
} }
override fun onScanFailed(errorCode: Int) { state.value = ConnectionState.DISCONNECTED } override fun onScanFailed(errorCode: Int) {
handler.removeCallbacks(restartScan)
state.value = ConnectionState.DISCONNECTED
scheduleReconnect()
}
} }
private val gattCallback = object : BluetoothGattCallback() { private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) { if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
this@BleClient.gatt = gatt this@BleClient.gatt = gatt
@@ -88,8 +114,10 @@ class BleClient(private val context: Context) {
if (!gatt.requestMtu(247)) gatt.discoverServices() if (!gatt.requestMtu(247)) gatt.discoverServices()
} }
else { else {
rx = null; gatt.close(); resetWrites(); state.value = ConnectionState.DISCONNECTED rx = null; gatt.close()
if (!stopped) handler.postDelayed({ connect() }, 3_000) if (this@BleClient.gatt === gatt) this@BleClient.gatt = null
resetWrites(); state.value = ConnectionState.DISCONNECTED
scheduleReconnect()
} }
} }
@@ -195,4 +223,10 @@ class BleClient(private val context: Context) {
writes.clear() writes.clear()
writeInProgress = false writeInProgress = false
} }
private fun scheduleReconnect(delayMillis: Long = 3_000) {
if (stopped) return
handler.removeCallbacks(reconnect)
handler.postDelayed(reconnect, delayMillis)
}
} }
Binary file not shown.
+1 -1
View File
@@ -7,7 +7,7 @@ Wants=pi-car-companion.service
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/local/libexec/pi-car-companion-bluetooth ExecStart=/usr/local/libexec/pi-car-companion-bluetooth
ExecStartPost=-/usr/bin/timeout 10 /usr/bin/btmgmt add-adv -c -u 6c7a0001-7c6d-4f74-9bb0-c5a4e5efc001 1 ExecStartPost=/usr/bin/timeout 5 /usr/bin/script -qec '/usr/bin/btmgmt add-adv -c -u 6c7a0001-7c6d-4f74-9bb0-c5a4e5efc001 1' /dev/null
Restart=on-failure Restart=on-failure
RestartSec=5s RestartSec=5s
NoNewPrivileges=true NoNewPrivileges=true