Работа с BLE через Android SDK доставляет немало хлопот. Мне бы хотелось, чтобы кто-нибудь помог мне с хорошей реализацией подключения BLE. Также было бы неплохо, если бы этот код был доступен и на других платформах через KMP.
Для периферийных подключений BLE я использую библиотеку Kable, которая поддерживает Android, iOS/macOS и Javascript. Благодаря моему ответу вы сможете реализовать свой класс для подключения к периферийным устройствам BLE менее чем за час. Чистая магия!
У этой библиотеки есть отличная небольшая документация, я рекомендую прочитать ее, чтобы понять особенности BLE в разных системах, прежде чем просматривать мой ответ.
Вначале я описал возможные состояния подключения. Они разделены на три разных типа: внутренняя реализация моего основного базового класса будет работать с типами Usable
, библиотека будет передавать проблемы из системы с использованием типов Unusable
, а в пользовательском интерфейсе также будет доступен тип NoPermissions
для отображения правильного состояния. в одном месте.
// package core.model
sealed interface BluetoothConnectionStatus {
/**
* Only on Android and iOS. It is implied that you will use this in the UI layer:
*
* ```
* val correctConnectionStatus =
* if (btPermissions.allPermissionsGranted) state.connectionStatus else BluetoothConnectionStatus.NoPermissions
* ```
*/
data object NoPermissions : BluetoothConnectionStatus
enum class Unusable : BluetoothConnectionStatus {
/** Bluetooth not available. */
UNAVAILABLE,
/**
* Only on Android 11 and below (on older systems, Bluetooth will not work if GPS is turned off).
*
* To enable, enable it via statusbar, use the Google Services API or go to location settings:
*
* ```
* fun Context.goToLocationSettings() = startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
* ```
*/
LOCATION_SHOULD_BE_ENABLED,
/**
* To enable (on Android), use this:
*
* ```
* fun Context.isPermissionProvided(permission: String): Boolean =
* ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
*
* @SuppressLint("MissingPermission")
* fun Context.enableBluetoothDialog() {
* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
* !isPermissionProvided(Manifest.permission.BLUETOOTH_CONNECT)
* ) {
* return
* }
*
* startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
* }
* ```
*/
DISABLED,
}
enum class Usable : BluetoothConnectionStatus {
ENABLED,
SCANNING,
CONNECTING,
CONNECTED,
}
}
Далее перейдем к основному классу, который будет использоваться для описания вашего устройства посредством наследования. Это дает вам возможность подключаться и работать с одним устройством, а если вы хотите подключиться к нескольким устройствам или устройствам разных типов, вам понадобится несколько объектов. В стандартном случае подключения только к одному устройству можно обойтись всего одним синглтоном.
// package core.ble.base
import com.juul.kable.Characteristic
import com.juul.kable.ObsoleteKableApi
import com.juul.kable.Peripheral
import com.juul.kable.PlatformAdvertisement
import com.juul.kable.ServicesDiscoveredPeripheral
import com.juul.kable.State
import com.juul.kable.WriteType
import com.juul.kable.characteristicOf
import com.juul.kable.logs.Logging
import core.model.BluetoothConnectionStatus
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.coroutineContext
// base UUID for predefined characteristics: 0000****-0000-1000-8000-00805f9b34fb
private const val RETRY_ATTEMPTS = 7
/** Used to quickly create classes to connect to BLE peripherals. */
abstract class BaseBleDevice(
private val serviceUuid: String,
private val platformBluetoothManager: PlatformBluetoothManager,
private val useKableLogging: Boolean = false,
) {
private var connectedPeripheral: Peripheral? = null
private val _connectionStatus: MutableStateFlow<BluetoothConnectionStatus.Usable> =
MutableStateFlow(BluetoothConnectionStatus.Usable.ENABLED)
/** Provides current connection status. */
val connectionStatus: Flow<BluetoothConnectionStatus> = combine(
platformBluetoothManager.systemBluetoothProblemStatus,
_connectionStatus,
) { problemStatusOrNull, internalStatus -> problemStatusOrNull ?: internalStatus }
/** Used to build public `connect` function. */
protected suspend fun scanAndConnect(
observeList: List<Pair<String, suspend (ByteArray) -> Unit>> = emptyList(),
onServicesDiscovered: suspend ServicesDiscoveredPeripheral.() -> Unit = {},
onSuccessfulConnect: suspend () -> Unit = {},
advertisementFilter: suspend PlatformAdvertisement.() -> Boolean = { true },
attempts: Int = RETRY_ATTEMPTS,
): Boolean {
if (!platformBluetoothManager.isPermissionsProvided ||
connectionStatus.first() != BluetoothConnectionStatus.Usable.ENABLED ||
attempts < 1
) {
return false
}
val coroutineScope = CoroutineScope(coroutineContext)
val peripheral = try {
_connectionStatus.value = BluetoothConnectionStatus.Usable.SCANNING
platformBluetoothManager.getFirstPeripheral(
coroutineScope = coroutineScope,
serviceUuid = serviceUuid,
advertisementFilter = advertisementFilter,
) {
if (useKableLogging) {
logging {
@OptIn(ObsoleteKableApi::class)
data = Logging.DataProcessor { data, _, _, _, _ ->
data.joinToString { byte -> byte.toString() }
}
level = Logging.Level.Data // Data > Events > Warnings
}
}
onServicesDiscovered(action = onServicesDiscovered)
}.also { coroutineScope.setupPeripheral(it, observeList) }
} catch (t: Throwable) {
Napier.e("scope.peripheral() exception caught: $t")
_connectionStatus.value = BluetoothConnectionStatus.Usable.ENABLED
coroutineContext.ensureActive()
return if (t !is UnsupportedOperationException && t !is CancellationException && attempts - 1 > 0) {
Napier.w("Retrying...")
scanAndConnect(
observeList = observeList,
onServicesDiscovered = onServicesDiscovered,
onSuccessfulConnect = onSuccessfulConnect,
advertisementFilter = advertisementFilter,
attempts = attempts - 1,
)
} else {
false
}
}
return connectToPeripheral(
peripheral = peripheral,
attempts = attempts,
onSuccessfulConnect = onSuccessfulConnect,
)
}
/** Used to build public `reconnect` function.
*
* Use it if you need fast reconnect, which will be cancelled in few seconds if peripheral won't found. */
protected suspend fun reconnect(onSuccessfulConnect: suspend () -> Unit = {}): Boolean {
if (connectionStatus.first() is BluetoothConnectionStatus.Unusable) {
return false
}
return connectedPeripheral?.let {
connectToPeripheral(
peripheral = it,
attempts = RETRY_ATTEMPTS,
onSuccessfulConnect = onSuccessfulConnect,
)
} ?: false
}
/** Call this function to disconnect the active connection.
*
* To cancel in-flight connection attempts you should cancel `Job` with running `connect`.
*
* If you are using `Job` cancellation to disconnect the active connection then you won't
* be able to use `reconnect` because `setupPeripheral` launches will be also cancelled.
*/
suspend fun disconnect() {
connectedPeripheral?.disconnect()
}
/**
* Can be used to create specified writing functions to send some values to the device.
*
* Set **`waitForResponse`** to
* * **`false`** if characteristic only supports `PROPERTY_WRITE_NO_RESPONSE`;
* * **`true`** if characteristic supports `PROPERTY_WRITE`.
*/
protected suspend fun writeTo(
characteristicUuid: String,
byteArray: ByteArray,
waitForResponse: Boolean,
attempts: Int = RETRY_ATTEMPTS,
): Boolean {
if (connectionStatus.first() is BluetoothConnectionStatus.Unusable) {
return false
}
connectedPeripheral?.let { currentPeripheral ->
repeat(attempts) {
try {
currentPeripheral.write(
characteristic = characteristicOf(serviceUuid, characteristicUuid),
data = byteArray,
writeType = if (waitForResponse) WriteType.WithResponse else WriteType.WithoutResponse,
)
return true
} catch (e: Exception) {
Napier.e("writeTo exception caught: $e")
coroutineContext.ensureActive()
if (e is CancellationException) {
return false
} else if (it != attempts - 1) {
Napier.w("Retrying...")
}
}
}
}
return false
}
/** Can be used to create specified reading functions to get some values from the device. */
protected suspend fun readFrom(
characteristicUuid: String,
attempts: Int = RETRY_ATTEMPTS,
readFunc: suspend (Characteristic) -> ByteArray? = { connectedPeripheral?.read(it) },
): ByteArray? {
if (connectionStatus.first() is BluetoothConnectionStatus.Unusable) {
return null
}
repeat(attempts) {
try {
return readFunc(characteristicOf(serviceUuid, characteristicUuid))
} catch (e: Exception) {
Napier.e("readFrom exception caught: $e")
coroutineContext.ensureActive()
if (e is CancellationException) {
return null
} else if (it != attempts - 1) {
Napier.w("Retrying...")
}
}
}
return null
}
/**
* Can be used to get some values from the device on services discovered.
*
* @see readFrom
*/
protected suspend fun ServicesDiscoveredPeripheral.readFrom(
characteristicUuid: String,
attempts: Int = RETRY_ATTEMPTS,
): ByteArray? = readFrom(characteristicUuid, attempts, ::read)
private fun CoroutineScope.setupPeripheral(
peripheral: Peripheral,
observeList: List<Pair<String, suspend (ByteArray) -> Unit>>,
) {
launch {
try {
var isCurrentlyStarted = true
peripheral.state.collect { currentState ->
if (currentState !is State.Disconnected || !isCurrentlyStarted) {
isCurrentlyStarted = false
_connectionStatus.value = when (currentState) {
is State.Disconnected,
State.Disconnecting,
-> BluetoothConnectionStatus.Usable.ENABLED
State.Connecting.Bluetooth,
State.Connecting.Services,
State.Connecting.Observes,
-> BluetoothConnectionStatus.Usable.CONNECTING
State.Connected,
-> BluetoothConnectionStatus.Usable.CONNECTED // or set it in connectToPeripheral()
}
}
}
} catch (t: Throwable) {
_connectionStatus.value = BluetoothConnectionStatus.Usable.ENABLED
coroutineContext.ensureActive()
}
}
observeList.forEach { (characteristic, observer) ->
launch {
peripheral.observe(
characteristicOf(service = serviceUuid, characteristic = characteristic),
).collect { byteArray ->
observer(byteArray)
}
}
}
}
private suspend fun connectToPeripheral(
peripheral: Peripheral,
attempts: Int,
onSuccessfulConnect: suspend () -> Unit,
): Boolean {
try {
peripheral.connect()
connectedPeripheral = peripheral
} catch (t: Throwable) {
Napier.e("connectToPeripheral exception caught: $t")
peripheral.disconnect()
_connectionStatus.value = BluetoothConnectionStatus.Usable.ENABLED
coroutineContext.ensureActive()
return if (t !is CancellationException && attempts - 1 > 0) {
Napier.w("Retrying...")
connectToPeripheral(
peripheral = peripheral,
attempts = attempts - 1,
onSuccessfulConnect = onSuccessfulConnect,
)
} else {
false
}
}
onSuccessfulConnect()
// _connectionStatus.value = BluetoothConnectionStatus.Usable.CONNECTED
return true
}
}
BaseBleDevice
зависит от PlatformBluetoothManager
— слоя KMP для работы с разными системами. Ожидаемые/фактические классы описаны ниже. Если ваш проект предназначен только для Android, вам нужно скопировать только класс androidMain и удалить из него ключевые слова actual
.
общийОсновной:
// package core.ble.base
import com.juul.kable.Peripheral
import com.juul.kable.PeripheralBuilder
import com.juul.kable.PlatformAdvertisement
import core.model.BluetoothConnectionStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
/** Provides system specific Bluetooth connectivity features for KMP targets. */
expect class PlatformBluetoothManager {
/**
* Provides flow with advertisements that can be used to show devices, select one of them,
* get `UUID`/`MAC` and use it in `advertisementFilter`.
*
* Not available in JS target.
*/
suspend fun getAdvertisements(serviceUuid: String): Flow<PlatformAdvertisement>
/** Provides first found peripheral to connect. */
suspend fun getFirstPeripheral(
coroutineScope: CoroutineScope,
serviceUuid: String,
advertisementFilter: suspend PlatformAdvertisement.() -> Boolean = { true }, // by default the first device found
peripheralBuilderAction: PeripheralBuilder.() -> Unit,
): Peripheral
/** Returns the Bluetooth status of the system: `Unusable` subtypes or null if it's `Usable`. */
val systemBluetoothProblemStatus: Flow<BluetoothConnectionStatus.Unusable?>
/** Indicates whether it is possible to start scanning and connection. */
val isPermissionsProvided: Boolean
}
androidОсновное:
// package core.ble.base
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import com.benasher44.uuid.uuidFrom
import com.juul.kable.Bluetooth
import com.juul.kable.Filter
import com.juul.kable.Peripheral
import com.juul.kable.PeripheralBuilder
import com.juul.kable.PlatformAdvertisement
import com.juul.kable.Reason
import com.juul.kable.Scanner
import com.juul.kable.peripheral
import core.model.BluetoothConnectionStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
actual class PlatformBluetoothManager(private val context: Context) {
actual suspend fun getAdvertisements(serviceUuid: String) =
Scanner { filters = listOf(Filter.Service(uuidFrom(serviceUuid))) }.advertisements
actual suspend fun getFirstPeripheral(
coroutineScope: CoroutineScope,
serviceUuid: String,
advertisementFilter: suspend PlatformAdvertisement.() -> Boolean,
peripheralBuilderAction: PeripheralBuilder.() -> Unit,
): Peripheral = coroutineScope.peripheral(
advertisement = getAdvertisements(serviceUuid).filter(predicate = advertisementFilter).first(),
builderAction = peripheralBuilderAction,
)
actual val systemBluetoothProblemStatus = Bluetooth.availability.map {
when (it) {
Bluetooth.Availability.Available -> null
is Bluetooth.Availability.Unavailable -> when (it.reason) {
Reason.Off -> BluetoothConnectionStatus.Unusable.DISABLED
Reason.LocationServicesDisabled -> BluetoothConnectionStatus.Unusable.LOCATION_SHOULD_BE_ENABLED
else -> BluetoothConnectionStatus.Unusable.UNAVAILABLE
}
}
}
actual val isPermissionsProvided
get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
context.isPermissionProvided(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
context.isPermissionProvided(Manifest.permission.BLUETOOTH_SCAN) &&
context.isPermissionProvided(Manifest.permission.BLUETOOTH_CONNECT)
}
}
private fun Context.isPermissionProvided(permission: String): Boolean =
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
jsMain:
// package core.ble.base
import com.benasher44.uuid.uuidFrom
import com.juul.kable.Bluetooth
import com.juul.kable.Filter
import com.juul.kable.Options
import com.juul.kable.Peripheral
import com.juul.kable.PeripheralBuilder
import com.juul.kable.PlatformAdvertisement
import com.juul.kable.requestPeripheral
import core.model.BluetoothConnectionStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.await
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
actual class PlatformBluetoothManager {
actual suspend fun getAdvertisements(serviceUuid: String): Flow<PlatformAdvertisement> =
throw NotImplementedError("Web Bluetooth doesn't allow to discover nearby devices")
actual suspend fun getFirstPeripheral(
coroutineScope: CoroutineScope,
serviceUuid: String,
advertisementFilter: suspend PlatformAdvertisement.() -> Boolean, // not used: in JS only user can select device
peripheralBuilderAction: PeripheralBuilder.() -> Unit,
): Peripheral = coroutineScope.requestPeripheral(
options = Options(filters = listOf(Filter.Service(uuidFrom(serviceUuid)))),
builderAction = peripheralBuilderAction,
).then(
onFulfilled = { it },
onRejected = {
throw UnsupportedOperationException(
"Can't show popup because user hasn't interacted with page or user has closed pairing popup",
)
},
).await()
actual val systemBluetoothProblemStatus = Bluetooth.availability.map {
when (it) {
is Bluetooth.Availability.Unavailable -> BluetoothConnectionStatus.Unusable.UNAVAILABLE
Bluetooth.Availability.Available -> null
}
}
actual val isPermissionsProvided = true
}
appleMain (поскольку у меня нет Mac для компиляции и тестирования, класс реализован не полностью):
// package core.ble.base
import com.benasher44.uuid.uuidFrom
import com.juul.kable.Bluetooth
import com.juul.kable.Filter
import com.juul.kable.Peripheral
import com.juul.kable.PeripheralBuilder
import com.juul.kable.PlatformAdvertisement
import com.juul.kable.Reason
import com.juul.kable.Scanner
import com.juul.kable.peripheral
import core.model.BluetoothConnectionStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
actual class PlatformBluetoothManager {
actual suspend fun getAdvertisements(serviceUuid: String) =
Scanner { filters = listOf(Filter.Service(uuidFrom(serviceUuid))) }.advertisements
actual suspend fun getFirstPeripheral(
coroutineScope: CoroutineScope,
serviceUuid: String,
advertisementFilter: suspend PlatformAdvertisement.() -> Boolean,
peripheralBuilderAction: PeripheralBuilder.() -> Unit,
): Peripheral = coroutineScope.peripheral(
advertisement = getAdvertisements(serviceUuid).filter(predicate = advertisementFilter).first(),
builderAction = peripheralBuilderAction,
)
actual val systemBluetoothProblemStatus = Bluetooth.availability.map {
when (it) {
Bluetooth.Availability.Available -> null
is Bluetooth.Availability.Unavailable -> when (it.reason) {
Reason.Off -> BluetoothConnectionStatus.Unusable.DISABLED
// Reason.Unauthorized -> BluetoothConnectionStatus.Unusable.UNAUTHORIZED // use it only if needed
else -> BluetoothConnectionStatus.Unusable.UNAVAILABLE
}
}
}
// I don't develop apps for Apple and don't have the ability to debug the code,
// so you'll have to add the implementation yourself.
actual val isPermissionsProvided: Boolean
get() = TODO()
}
Это основная часть, которую вам нужно скопировать себе, чтобы быстро реализовать связь с BLE-устройствами.
Ниже вы можете увидеть пример класса для работы со специальным RFID-считывателем, который вы можете использовать в качестве примера для создания своего собственного класса. Новые значения передаются слушателям через SharedFlow
и StateFlow
. При подключении мы инициализируем прослушивание обновлений характеристик через observeList
, заставляем некоторые из них читаться сразу после подключения в onServicesDiscovered
и требуем подключаться только к пропущенным macAddress
через advertisementFilter
. Кроме того, класс позволяет получить connectionStatus
, поскольку он наследуется от BaseBleDevice
и позволяет передавать время устройству через sendTime()
. Обратите внимание, что этот класс использует функции, специфичные для платформы Android, несовместим с KMP и представлен только в качестве примера.
// package core.ble.laundry
import core.ble.base.BaseBleDevice
import core.ble.base.PlatformBluetoothManager
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.datetime.Clock
import javax.inject.Inject
import javax.inject.Singleton
private const val SERVICE_UUID = "f71381b8-c439-4b29-8256-620efaef0b4e"
// here you can also use Bluetooth.BaseUuid.plus(0x2a19).toString()
private const val BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb"
private const val BATTERY_IS_CHARGING_UUID = "1170f274-a09b-46b3-88c5-0e1c67037861"
private const val RFID_UUID = "3d58f98d-63f0-43d5-a7d4-54fa1ed824ba"
private const val TIME_UUID = "016c2726-b22a-4cdc-912b-f626d1e4051e"
@Singleton
class PersonProviderDevice @Inject constructor(
platformBluetoothManager: PlatformBluetoothManager,
) : BaseBleDevice(SERVICE_UUID, platformBluetoothManager) {
private val _providedRfid = MutableSharedFlow<Long>()
val providedRfid: SharedFlow<Long> = _providedRfid
private val _batteryState = MutableStateFlow(BatteryState())
val batteryState: StateFlow<BatteryState> = _batteryState
suspend fun connect(macAddress: String?) = scanAndConnect(
observeList = listOf(
RFID_UUID to { bytes ->
bytes.toULong()?.let { _providedRfid.emit(it.toLong()) }
},
BATTERY_LEVEL_UUID to { bytes ->
_batteryState.value = _batteryState.value.copy(level = bytes.toBatteryLevel())
},
BATTERY_IS_CHARGING_UUID to { bytes ->
_batteryState.value = _batteryState.value.copy(isCharging = bytes.toIsCharging())
},
),
onServicesDiscovered = {
// requestMtu(512)
val level = this.readFrom(BATTERY_LEVEL_UUID)?.toBatteryLevel()
val isCharging = this.readFrom(BATTERY_IS_CHARGING_UUID)?.toIsCharging()
_batteryState.value = BatteryState(level, isCharging ?: false)
},
advertisementFilter = { macAddress?.let { this.address.lowercase() == it.lowercase() } ?: true },
)
suspend fun sendTime(): Boolean {
val byteArrayWithTime = Clock.System.now().toEpochMilliseconds().toString().toByteArray()
return writeTo(TIME_UUID, byteArrayWithTime, true)
}
private fun ByteArray.toBatteryLevel() = this.first().toInt()
private fun ByteArray.toIsCharging() = this.first().toInt() == 1
}
/**
* Provides ULong from ByteArray encoded with little endian
*/
fun ByteArray.toULong(size: Int = 8): ULong? =
if (this.size != size) {
null
} else {
var result: ULong = 0u
repeat(size) {
result = result or ((this[it].toULong() and 0xFFu) shl (it * 8))
}
result
}
data class BatteryState(
val level: Int? = null,
val isCharging: Boolean = false,
)
Этот класс предназначен для работы только с одним устройством одновременно. Он не предоставляет возможности отключиться от устройства через стандартную disconnect()
функцию, но возможен через Job
отмену. reconnect()
также недоступен, поскольку требуется в очень редких случаях.
Для работы с этим классом я добавил следующий код в свой ViewModel
. Он позволяет автоматически отключаться от устройства, когда приложение свернуто, автоматически подключаться, когда приложение активно и есть возможность подключения. Вам нужно вызвать onResume() и onStop() из соответствующих функций в вашем Activity
.
private var isLaunchingConnectionJob = false
private var connection: Job? = null
private var isAppResumed: Boolean = false
init {
viewModelScope.launch {
rfidRepository.connectionStatus.collect {
state = state.copy(connectionStatus = it)
// used to reconnect when device was disconnected
connectIfResumedAndNotConnectedOrSendTime()
}
}
}
fun onResume() {
isAppResumed = true
viewModelScope.launch {
connectIfResumedAndNotConnectedOrSendTime()
}
}
fun onStop() {
isAppResumed = false
viewModelScope.launch {
delay(2000)
if (!isAppResumed) {
connection?.cancelAndJoin()
}
}
}
private suspend fun connectIfResumedAndNotConnectedOrSendTime() {
if (isLaunchingConnectionJob) {
return
}
isLaunchingConnectionJob = true
if (isAppResumed && rfidRepository.connectionStatus.first() == BluetoothConnectionStatus.Usable.ENABLED) {
connection?.cancelAndJoin()
connection = viewModelScope.launch {
isLaunchingConnectionJob = false
rfidRepository.connect()
while (true) {
delay(1000)
val isWritten = rfidRepository.sendTime()
if (!isWritten) {
connection?.cancelAndJoin()
}
}
}
} else {
isLaunchingConnectionJob = false
}
}
Итак, для работы с BLE-устройствами вам достаточно скопировать классы BluetoothConnectionStatus
, BaseBleDevice
и PlatformBluetoothManager
в свой проект, самостоятельно описать характеристики вашего устройства, создав наследника BaseBleDevice
и подключиться к устройству из класса ViewModel
.