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:
James Rich 2026-03-06 20:43:45 -06:00 committed by GitHub
parent 182ad933f4
commit 0ce322a0f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
163 changed files with 1837 additions and 877 deletions

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}
}
}

View file

@ -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) }
}

View file

@ -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

View file

@ -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 =

View file

@ -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.
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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>
}

View file

@ -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(),
)

View file

@ -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)
}