mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Migrate project to Kotlin Multiplatform (KMP) architecture (#4738)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
182ad933f4
commit
0ce322a0f5
163 changed files with 1837 additions and 877 deletions
|
|
@ -34,92 +34,85 @@ import kotlinx.coroutines.withTimeout
|
|||
import no.nordicsemi.android.common.core.simpleSharedFlow
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Encapsulates a BLE connection to a [Peripheral]. Handles connection lifecycle, state monitoring, and service
|
||||
* discovery.
|
||||
* An Android implementation of [BleConnection] using Nordic's [CentralManager].
|
||||
*
|
||||
* @param centralManager The Nordic [CentralManager] to use for connection.
|
||||
* @param scope The [CoroutineScope] in which to monitor connection state.
|
||||
* @param tag A tag for logging.
|
||||
*/
|
||||
class BleConnection(
|
||||
class AndroidBleConnection(
|
||||
private val centralManager: CentralManager,
|
||||
private val scope: CoroutineScope,
|
||||
private val tag: String = "BLE",
|
||||
) {
|
||||
/** The currently connected [Peripheral], or null if not connected. */
|
||||
var peripheral: Peripheral? = null
|
||||
private set
|
||||
) : BleConnection {
|
||||
|
||||
private val _peripheral = MutableSharedFlow<Peripheral?>(replay = 1)
|
||||
private var _device: AndroidBleDevice? = null
|
||||
override val device: BleDevice?
|
||||
get() = _device
|
||||
|
||||
/** A flow of the current peripheral. */
|
||||
val peripheralFlow = _peripheral.asSharedFlow()
|
||||
private val _deviceFlow = MutableSharedFlow<BleDevice?>(replay = 1)
|
||||
override val deviceFlow: SharedFlow<BleDevice?> = _deviceFlow.asSharedFlow()
|
||||
|
||||
private val _connectionState = simpleSharedFlow<ConnectionState>()
|
||||
|
||||
/** A flow of [ConnectionState] changes for the current [peripheral]. */
|
||||
val connectionState: SharedFlow<ConnectionState> = _connectionState.asSharedFlow()
|
||||
private val _connectionState = simpleSharedFlow<BleConnectionState>()
|
||||
override val connectionState: SharedFlow<BleConnectionState> = _connectionState.asSharedFlow()
|
||||
|
||||
private var stateJob: Job? = null
|
||||
private var profileJob: Job? = null
|
||||
|
||||
/**
|
||||
* Connects to the given [Peripheral]. Note that this method returns as soon as the connection attempt is initiated.
|
||||
* Use [connectAndAwait] if you need to wait for the connection to be established.
|
||||
*
|
||||
* @param p The peripheral to connect to.
|
||||
*/
|
||||
suspend fun connect(p: Peripheral) = withContext(NonCancellable) {
|
||||
override suspend fun connect(device: BleDevice) = withContext(NonCancellable) {
|
||||
val androidDevice = device as AndroidBleDevice
|
||||
stateJob?.cancel()
|
||||
peripheral = p
|
||||
_peripheral.emit(p)
|
||||
_device = androidDevice
|
||||
_deviceFlow.emit(androidDevice)
|
||||
|
||||
centralManager.connect(
|
||||
peripheral = p,
|
||||
peripheral = androidDevice.peripheral,
|
||||
options = CentralManager.ConnectionOptions.AutoConnect(automaticallyRequestHighestValueLength = true),
|
||||
)
|
||||
|
||||
stateJob =
|
||||
p.state
|
||||
androidDevice.peripheral.state
|
||||
.onEach { state ->
|
||||
Logger.d { "[$tag] Connection state changed to $state" }
|
||||
val commonState =
|
||||
when (state) {
|
||||
is ConnectionState.Connecting -> BleConnectionState.Connecting
|
||||
is ConnectionState.Connected -> BleConnectionState.Connected
|
||||
is ConnectionState.Disconnecting -> BleConnectionState.Disconnecting
|
||||
is ConnectionState.Disconnected -> BleConnectionState.Disconnected
|
||||
}
|
||||
|
||||
if (state is ConnectionState.Connected) {
|
||||
p.requestConnectionPriority(ConnectionPriority.HIGH)
|
||||
observePeripheralDetails(p)
|
||||
androidDevice.peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
|
||||
observePeripheralDetails(androidDevice)
|
||||
}
|
||||
|
||||
_connectionState.emit(state)
|
||||
androidDevice.updateState(state)
|
||||
_connectionState.emit(commonState)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the given [Peripheral] and waits for a terminal state (Connected or Disconnected).
|
||||
*
|
||||
* @param p The peripheral to connect to.
|
||||
* @param timeoutMs The maximum time to wait for a connection in milliseconds.
|
||||
* @param onRegister Optional block to run before connecting, allowing for profile registration.
|
||||
* @return The final [ConnectionState].
|
||||
* @throws kotlinx.coroutines.TimeoutCancellationException if the timeout is reached.
|
||||
*/
|
||||
suspend fun connectAndAwait(p: Peripheral, timeoutMs: Long, onRegister: suspend () -> Unit = {}): ConnectionState {
|
||||
override suspend fun connectAndAwait(
|
||||
device: BleDevice,
|
||||
timeoutMs: Long,
|
||||
onRegister: suspend () -> Unit,
|
||||
): BleConnectionState {
|
||||
onRegister()
|
||||
connect(p)
|
||||
connect(device)
|
||||
return withTimeout(timeoutMs) {
|
||||
connectionState.first { it is ConnectionState.Connected || it is ConnectionState.Disconnected }
|
||||
connectionState.first { it is BleConnectionState.Connected || it is BleConnectionState.Disconnected }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun observePeripheralDetails(p: Peripheral) {
|
||||
private fun observePeripheralDetails(androidDevice: AndroidBleDevice) {
|
||||
val p = androidDevice.peripheral
|
||||
p.phy.onEach { phy -> Logger.i { "[$tag] BLE PHY changed to $phy" } }.launchIn(scope)
|
||||
|
||||
p.connectionParameters
|
||||
|
|
@ -135,32 +128,24 @@ class BleConnection(
|
|||
.launchIn(scope)
|
||||
}
|
||||
|
||||
/** Disconnects from the current peripheral. */
|
||||
suspend fun disconnect() = withContext(NonCancellable) {
|
||||
override suspend fun disconnect() = withContext(NonCancellable) {
|
||||
stateJob?.cancel()
|
||||
stateJob = null
|
||||
profileJob?.cancel()
|
||||
profileJob = null
|
||||
peripheral?.disconnect()
|
||||
peripheral = null
|
||||
_peripheral.emit(null)
|
||||
_device?.peripheral?.disconnect()
|
||||
_device = null
|
||||
_deviceFlow.emit(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a block within a discovered profile. Handles peripheral readiness, discovery with a timeout, and cleans
|
||||
* up the profile job if discovery fails.
|
||||
*
|
||||
* @param serviceUuid The UUID of the service to discover.
|
||||
* @param timeout The duration to wait for discovery.
|
||||
* @param block The block to execute with the discovered service.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
suspend fun <T> profile(
|
||||
override suspend fun <T> profile(
|
||||
serviceUuid: Uuid,
|
||||
timeout: kotlin.time.Duration = 30.seconds,
|
||||
setup: suspend CoroutineScope.(no.nordicsemi.kotlin.ble.client.RemoteService) -> T,
|
||||
timeout: kotlin.time.Duration,
|
||||
setup: suspend CoroutineScope.(BleService) -> T,
|
||||
): T {
|
||||
val p = peripheralFlow.first { it != null }!!
|
||||
val androidDevice = deviceFlow.first { it != null } as AndroidBleDevice
|
||||
val p = androidDevice.peripheral
|
||||
val serviceReady = CompletableDeferred<T>()
|
||||
|
||||
profileJob?.cancel()
|
||||
|
|
@ -170,9 +155,8 @@ class BleConnection(
|
|||
val profileScope = this
|
||||
p.profile(serviceUuid = serviceUuid, required = true, scope = profileScope) { service ->
|
||||
try {
|
||||
val result = setup(service)
|
||||
val result = setup(AndroidBleService(service))
|
||||
serviceReady.complete(result)
|
||||
// Keep the profile active until this launch scope (profileJob) is cancelled
|
||||
awaitCancellation()
|
||||
} catch (e: Throwable) {
|
||||
if (!serviceReady.isCompleted) serviceReady.completeExceptionally(e)
|
||||
|
|
@ -193,11 +177,17 @@ class BleConnection(
|
|||
}
|
||||
}
|
||||
|
||||
/** Returns the maximum write value length for the given write type. */
|
||||
fun maximumWriteValueLength(writeType: WriteType): Int? = peripheral?.maximumWriteValueLength(writeType)
|
||||
override fun maximumWriteValueLength(writeType: BleWriteType): Int? {
|
||||
val nordicWriteType =
|
||||
when (writeType) {
|
||||
BleWriteType.WITH_RESPONSE -> WriteType.WITH_RESPONSE
|
||||
BleWriteType.WITHOUT_RESPONSE -> WriteType.WITHOUT_RESPONSE
|
||||
}
|
||||
return _device?.peripheral?.maximumWriteValueLength(nordicWriteType)
|
||||
}
|
||||
|
||||
/** Requests a new connection priority for the current peripheral. */
|
||||
suspend fun requestConnectionPriority(priority: ConnectionPriority) {
|
||||
peripheral?.requestConnectionPriority(priority)
|
||||
_device?.peripheral?.requestConnectionPriority(priority)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,20 +16,15 @@
|
|||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** A snapshot in time of the state of the bluetooth subsystem. */
|
||||
data class BluetoothState(
|
||||
/** Whether we have adequate permissions to query bluetooth state */
|
||||
val hasPermissions: Boolean = false,
|
||||
/** If we have adequate permissions and bluetooth is enabled */
|
||||
val enabled: Boolean = false,
|
||||
/** If enabled, a list of the currently bonded devices */
|
||||
val bondedDevices: List<Peripheral> = emptyList(),
|
||||
) {
|
||||
override fun toString(): String =
|
||||
"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map {
|
||||
it.anonymize
|
||||
}})"
|
||||
/** An Android implementation of [BleConnectionFactory]. */
|
||||
@Singleton
|
||||
class AndroidBleConnectionFactory @Inject constructor(private val centralManager: CentralManager) :
|
||||
BleConnectionFactory {
|
||||
override fun create(scope: CoroutineScope, tag: String): BleConnection =
|
||||
AndroidBleConnection(centralManager, scope, tag)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import no.nordicsemi.kotlin.ble.core.BondState
|
||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||
|
||||
/** An Android implementation of [BleDevice] that wraps a Nordic [Peripheral]. */
|
||||
class AndroidBleDevice(val peripheral: Peripheral) : BleDevice {
|
||||
override val name: String?
|
||||
get() = peripheral.name
|
||||
|
||||
override val address: String
|
||||
get() = peripheral.address
|
||||
|
||||
private val _state = MutableStateFlow<BleConnectionState>(BleConnectionState.Disconnected)
|
||||
override val state: StateFlow<BleConnectionState> = _state.asStateFlow()
|
||||
|
||||
@Suppress("MissingPermission")
|
||||
override val isBonded: Boolean
|
||||
get() = peripheral.bondState.value == BondState.BONDED
|
||||
|
||||
override val isConnected: Boolean
|
||||
get() = peripheral.isConnected
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun readRssi(): Int = peripheral.readRssi()
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun bond() {
|
||||
peripheral.createBond()
|
||||
}
|
||||
|
||||
/** Updates the connection state based on Nordic's [ConnectionState]. */
|
||||
fun updateState(nordicState: ConnectionState) {
|
||||
_state.value =
|
||||
when (nordicState) {
|
||||
is ConnectionState.Connecting -> BleConnectionState.Connecting
|
||||
is ConnectionState.Connected -> BleConnectionState.Connected
|
||||
is ConnectionState.Disconnecting -> BleConnectionState.Disconnecting
|
||||
is ConnectionState.Disconnected -> BleConnectionState.Disconnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,33 +19,17 @@ package org.meshtastic.core.ble
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.ConjunctionFilterScope
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import no.nordicsemi.kotlin.ble.client.distinctByPeripheral
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* A wrapper around [CentralManager]'s scanning capabilities to provide a consistent and easy-to-use API for BLE
|
||||
* scanning across the application.
|
||||
* An Android implementation of [BleScanner] using Nordic's [CentralManager].
|
||||
*
|
||||
* @param centralManager The Nordic [CentralManager] to use for scanning.
|
||||
*/
|
||||
class BleScanner @Inject constructor(private val centralManager: CentralManager) {
|
||||
class AndroidBleScanner @Inject constructor(private val centralManager: CentralManager) : BleScanner {
|
||||
|
||||
/**
|
||||
* Scans for BLE devices.
|
||||
*
|
||||
* @param timeout The duration of the scan.
|
||||
* @param filterBlock Optional filter configuration block.
|
||||
* @return A [Flow] of discovered [Peripheral]s.
|
||||
*/
|
||||
fun scan(timeout: Duration, filterBlock: (ConjunctionFilterScope.() -> Unit)? = null): Flow<Peripheral> =
|
||||
if (filterBlock != null) {
|
||||
centralManager.scan(timeout, filterBlock)
|
||||
} else {
|
||||
centralManager.scan(timeout)
|
||||
}
|
||||
.distinctByPeripheral()
|
||||
.map { it.peripheral }
|
||||
override fun scan(timeout: Duration): Flow<BleDevice> =
|
||||
centralManager.scan(timeout).distinctByPeripheral().map { AndroidBleDevice(it.peripheral) }
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||
|
||||
/** An Android implementation of [BleService] that wraps a Nordic [RemoteService]. */
|
||||
class AndroidBleService(val service: RemoteService) : BleService
|
||||
|
|
@ -36,26 +36,18 @@ import org.meshtastic.core.di.ProcessLifecycle
|
|||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Repository responsible for maintaining and updating the state of Bluetooth availability. */
|
||||
/** Android implementation of [BluetoothRepository]. */
|
||||
@Singleton
|
||||
class BluetoothRepository
|
||||
class AndroidBluetoothRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
@ProcessLifecycle private val processLifecycle: Lifecycle,
|
||||
private val centralManager: CentralManager,
|
||||
private val androidEnvironment: AndroidEnvironment,
|
||||
) {
|
||||
private val _state =
|
||||
MutableStateFlow(
|
||||
BluetoothState(
|
||||
// Assume we have permission until we get our initial state update to prevent premature
|
||||
// notifications to the user.
|
||||
hasPermissions = true,
|
||||
),
|
||||
)
|
||||
val state: StateFlow<BluetoothState>
|
||||
get() = _state.asStateFlow()
|
||||
) : BluetoothRepository {
|
||||
private val _state = MutableStateFlow(BluetoothState(hasPermissions = true))
|
||||
override val state: StateFlow<BluetoothState> = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
|
|
@ -63,25 +55,16 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun refreshState() {
|
||||
override fun refreshState() {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) { updateBluetoothState() }
|
||||
}
|
||||
|
||||
/** @return true for a valid Bluetooth address, false otherwise */
|
||||
fun isValid(bleAddress: String): Boolean = BluetoothAdapter.checkBluetoothAddress(bleAddress)
|
||||
override fun isValid(bleAddress: String): Boolean = BluetoothAdapter.checkBluetoothAddress(bleAddress)
|
||||
|
||||
/**
|
||||
* Initiates bonding with the given peripheral. This is a suspending function that completes when the bonding
|
||||
* process is finished. After successful bonding, the repository's state is refreshed to include the new bonded
|
||||
* device.
|
||||
*
|
||||
* @param peripheral The peripheral to bond with.
|
||||
* @throws SecurityException if required Bluetooth permissions are not granted.
|
||||
* @throws Exception if the bonding process fails.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
suspend fun bond(peripheral: Peripheral) {
|
||||
peripheral.createBond()
|
||||
override suspend fun bond(device: BleDevice) {
|
||||
val androidDevice = device as AndroidBleDevice
|
||||
androidDevice.peripheral.createBond()
|
||||
updateBluetoothState()
|
||||
}
|
||||
|
||||
|
|
@ -100,16 +83,15 @@ constructor(
|
|||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun getBondedAppPeripherals(enabled: Boolean, hasPerms: Boolean): List<Peripheral> =
|
||||
private fun getBondedAppPeripherals(enabled: Boolean, hasPerms: Boolean): List<BleDevice> =
|
||||
if (enabled && hasPerms) {
|
||||
centralManager.getBondedPeripherals().filter(::isMatchingPeripheral)
|
||||
centralManager.getBondedPeripherals().filter(::isMatchingPeripheral).map { AndroidBleDevice(it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
/** @return true if the given address is currently bonded to the system. */
|
||||
@SuppressLint("MissingPermission")
|
||||
fun isBonded(address: String): Boolean {
|
||||
override fun isBonded(address: String): Boolean {
|
||||
val enabled = androidEnvironment.isBluetoothEnabled
|
||||
val hasPerms = hasRequiredPermissions()
|
||||
return if (enabled && hasPerms) {
|
||||
|
|
@ -126,7 +108,6 @@ constructor(
|
|||
androidEnvironment.isLocationPermissionGranted
|
||||
}
|
||||
|
||||
/** Checks if a peripheral is one of ours, either by its advertised name or by the services it provides. */
|
||||
private fun isMatchingPeripheral(peripheral: Peripheral): Boolean {
|
||||
val nameMatches = peripheral.name?.matches(Regex(BLE_NAME_PATTERN)) ?: false
|
||||
val hasRequiredService =
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/** Represents the type of write operation. */
|
||||
enum class BleWriteType {
|
||||
WITH_RESPONSE,
|
||||
WITHOUT_RESPONSE,
|
||||
}
|
||||
|
||||
/** Encapsulates a BLE connection to a [BleDevice]. */
|
||||
interface BleConnection {
|
||||
/** The currently connected [BleDevice], or null if not connected. */
|
||||
val device: BleDevice?
|
||||
|
||||
/** A flow of the current device. */
|
||||
val deviceFlow: SharedFlow<BleDevice?>
|
||||
|
||||
/** A flow of [BleConnectionState] changes. */
|
||||
val connectionState: SharedFlow<BleConnectionState>
|
||||
|
||||
/** Connects to the given [BleDevice]. */
|
||||
suspend fun connect(device: BleDevice)
|
||||
|
||||
/** Connects to the given [BleDevice] and waits for a terminal state. */
|
||||
suspend fun connectAndAwait(
|
||||
device: BleDevice,
|
||||
timeoutMs: Long,
|
||||
onRegister: suspend () -> Unit = {},
|
||||
): BleConnectionState
|
||||
|
||||
/** Disconnects from the current device. */
|
||||
suspend fun disconnect()
|
||||
|
||||
/** Executes a block within a discovered profile. */
|
||||
suspend fun <T> profile(
|
||||
serviceUuid: Uuid,
|
||||
timeout: Duration = 30.seconds,
|
||||
setup: suspend CoroutineScope.(BleService) -> T,
|
||||
): T
|
||||
|
||||
/** Returns the maximum write value length for the given write type. */
|
||||
fun maximumWriteValueLength(writeType: BleWriteType): Int?
|
||||
}
|
||||
|
||||
/** Represents a BLE service for commonMain. */
|
||||
interface BleService {
|
||||
// This will be expanded as needed, but for now we just need a common type to pass around.
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/** A factory for creating [BleConnection] instances. */
|
||||
interface BleConnectionFactory {
|
||||
/**
|
||||
* Creates a new [BleConnection] instance.
|
||||
*
|
||||
* @param scope The [CoroutineScope] in which to monitor connection state.
|
||||
* @param tag A tag for logging.
|
||||
* @return A new [BleConnection] instance.
|
||||
*/
|
||||
fun create(scope: CoroutineScope, tag: String): BleConnection
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
/** Represents the state of a BLE connection. */
|
||||
sealed class BleConnectionState {
|
||||
/** The peripheral is disconnected. */
|
||||
object Disconnected : BleConnectionState()
|
||||
|
||||
/** The peripheral is connecting. */
|
||||
object Connecting : BleConnectionState()
|
||||
|
||||
/** The peripheral is connected. */
|
||||
object Connected : BleConnectionState()
|
||||
|
||||
/** The peripheral is disconnecting. */
|
||||
object Disconnecting : BleConnectionState()
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Represents a BLE device. */
|
||||
interface BleDevice {
|
||||
/** The device's name. */
|
||||
val name: String?
|
||||
|
||||
/** The device's address. */
|
||||
val address: String
|
||||
|
||||
/** The current connection state of the device. */
|
||||
val state: StateFlow<BleConnectionState>
|
||||
|
||||
/** Whether the device is bonded. */
|
||||
val isBonded: Boolean
|
||||
|
||||
/** Whether the device is currently connected. */
|
||||
val isConnected: Boolean
|
||||
|
||||
/** Reads the current RSSI value. */
|
||||
suspend fun readRssi(): Int
|
||||
|
||||
/** Bond the device. */
|
||||
suspend fun bond()
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Duration
|
||||
|
||||
/** A scanner for BLE devices. */
|
||||
interface BleScanner {
|
||||
/**
|
||||
* Scans for BLE devices.
|
||||
*
|
||||
* @param timeout The duration of the scan.
|
||||
* @return A [Flow] of discovered [BleDevice]s.
|
||||
*/
|
||||
fun scan(timeout: Duration): Flow<BleDevice>
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Repository responsible for Bluetooth availability and bonding. */
|
||||
interface BluetoothRepository {
|
||||
/** The current state of Bluetooth on the device. */
|
||||
val state: StateFlow<BluetoothState>
|
||||
|
||||
/** Refreshes the Bluetooth state. */
|
||||
fun refreshState()
|
||||
|
||||
/** Returns true if the given address is valid. */
|
||||
fun isValid(bleAddress: String): Boolean
|
||||
|
||||
/** Returns true if the given address is bonded. */
|
||||
fun isBonded(address: String): Boolean
|
||||
|
||||
/** Initiates bonding with the given device. */
|
||||
suspend fun bond(device: BleDevice)
|
||||
}
|
||||
|
||||
/** Represents the state of Bluetooth on the device. */
|
||||
data class BluetoothState(
|
||||
/** True if the application has the required Bluetooth permissions. */
|
||||
val hasPermissions: Boolean = false,
|
||||
|
||||
/** True if Bluetooth is enabled on the device. */
|
||||
val enabled: Boolean = false,
|
||||
|
||||
/** A list of bonded devices. */
|
||||
val bondedDevices: List<BleDevice> = emptyList(),
|
||||
)
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.native
|
||||
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
|
||||
import no.nordicsemi.kotlin.ble.environment.android.NativeAndroidEnvironment
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object BleModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAndroidEnvironment(@ApplicationContext context: Context): AndroidEnvironment =
|
||||
NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCentralManager(environment: AndroidEnvironment, coroutineScope: CoroutineScope): CentralManager =
|
||||
CentralManager.native(environment as NativeAndroidEnvironment, coroutineScope)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBleSingletonCoroutineScope(dispatchers: CoroutineDispatchers): CoroutineScope =
|
||||
CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue