Как мне подключиться по BLE к периферийному устройству с помощью Kotlin Multiplatform или собственного Android?

Работа с BLE через Android SDK доставляет немало хлопот. Мне бы хотелось, чтобы кто-нибудь помог мне с хорошей реализацией подключения BLE. Также было бы неплохо, если бы этот код был доступен и на других платформах через KMP.

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
0
103
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Для периферийных подключений 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.

Другие вопросы по теме

Похожие вопросы

Как объединить разметку рейтинговой панели компонента звездного рейтинга с помощью цикла for?
Как проверить, что значение флажка содержит строковое значение из ввода в JavaScript
Когда я устанавливаю флажок, он отлично отображает свое значение. Когда я отменяю его выбор, он продолжает отображаться
Добавление проверок в поле «Номер контакта» с помощью ASP.NET Core
JavaScript Eval: в случае ошибки отображать правильную ссылку на скрипт в консоли
Gulp imagemin разбивает изображения и не оптимизирует
OneTrust: проверьте, используется ли OneTrust в проекте, и дождитесь полной загрузки
Как заполнить текстовое поле с помощью php ajax в каждой строке таблицы при изменении другого текстового поля в строке
Какие функции передаются в Promise при ожидании в JavaScript?
Как создать диаграммы на моем веб-сайте JavaScript, используя данные из базы данных MySQL. Использование внешнего интерфейса EJS