mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: BLE transport and UI for Kotlin Multiplatform unification (#4911)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
b0e91a390c
commit
6516287c62
42 changed files with 429 additions and 845 deletions
|
|
@ -19,31 +19,42 @@ package org.meshtastic.core.network.radio
|
|||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.RadioTransport
|
||||
import org.meshtastic.core.repository.RadioTransportFactory
|
||||
|
||||
/** Android implementation of [RadioTransportFactory] delegating to the legacy [InterfaceFactory]. */
|
||||
@Single
|
||||
/**
|
||||
* Android implementation of [RadioTransportFactory]. Handles pure-KMP transports (BLE) via [BaseRadioTransportFactory]
|
||||
* while delegating legacy platform-specific connections (like USB/Serial, TCP, and Mocks) to the Android-specific
|
||||
* [InterfaceFactory].
|
||||
*/
|
||||
@Single(binds = [RadioTransportFactory::class])
|
||||
@Suppress("LongParameterList")
|
||||
class AndroidRadioTransportFactory(
|
||||
private val context: Context,
|
||||
private val interfaceFactory: Lazy<InterfaceFactory>,
|
||||
private val buildConfigProvider: BuildConfigProvider,
|
||||
) : RadioTransportFactory {
|
||||
scanner: BleScanner,
|
||||
bluetoothRepository: BluetoothRepository,
|
||||
connectionFactory: BleConnectionFactory,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : BaseRadioTransportFactory(scanner, bluetoothRepository, connectionFactory, dispatchers) {
|
||||
|
||||
override val supportedDeviceTypes: List<DeviceType> = listOf(DeviceType.BLE, DeviceType.TCP, DeviceType.USB)
|
||||
|
||||
override fun isMockInterface(): Boolean =
|
||||
buildConfigProvider.isDebug || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
|
||||
|
||||
override fun createTransport(address: String, service: RadioInterfaceService): RadioTransport =
|
||||
interfaceFactory.value.createInterface(address, service)
|
||||
override fun isPlatformAddressValid(address: String): Boolean = interfaceFactory.value.addressValid(address)
|
||||
|
||||
override fun isAddressValid(address: String?): Boolean = interfaceFactory.value.addressValid(address)
|
||||
|
||||
override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
|
||||
interfaceFactory.value.toInterfaceAddress(interfaceId, rest)
|
||||
override fun createPlatformTransport(address: String, service: RadioInterfaceService): RadioTransport {
|
||||
// Fallback to legacy factory for Serial, Mocks, and NOPs
|
||||
return interfaceFactory.value.createInterface(address, service)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +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.network.radio
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
/** Factory for creating `BleRadioInterface` instances. */
|
||||
@Single
|
||||
class BleRadioInterfaceFactory(
|
||||
private val scanner: BleScanner,
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
private val connectionFactory: BleConnectionFactory,
|
||||
) {
|
||||
fun create(rest: String, service: RadioInterfaceService): BleRadioInterface = BleRadioInterface(
|
||||
serviceScope = service.serviceScope,
|
||||
scanner = scanner,
|
||||
bluetoothRepository = bluetoothRepository,
|
||||
connectionFactory = connectionFactory,
|
||||
service = service,
|
||||
address = rest,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,34 +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.network.radio
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
/** Bluetooth backend implementation. */
|
||||
@Single
|
||||
class BleRadioInterfaceSpec(private val factory: BleRadioInterfaceFactory) : InterfaceSpec<BleRadioInterface> {
|
||||
override fun createInterface(rest: String, service: RadioInterfaceService): BleRadioInterface =
|
||||
factory.create(rest, service)
|
||||
|
||||
/** Return true if this address is still acceptable. For Kable we don't strictly require prior bonding. */
|
||||
override fun addressValid(rest: String): Boolean {
|
||||
// We no longer strictly require the device to be in the bonded list before attempting connection,
|
||||
// as Kable and Android will handle bonding seamlessly during connection/characteristic access if needed.
|
||||
return rest.isNotBlank()
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ import org.meshtastic.core.repository.RadioTransport
|
|||
@Single
|
||||
class InterfaceFactory(
|
||||
private val nopInterfaceFactory: NopInterfaceFactory,
|
||||
private val bluetoothSpec: Lazy<BleRadioInterfaceSpec>,
|
||||
private val mockSpec: Lazy<MockInterfaceSpec>,
|
||||
private val serialSpec: Lazy<SerialInterfaceSpec>,
|
||||
private val tcpSpec: Lazy<TCPInterfaceSpec>,
|
||||
|
|
@ -40,7 +39,6 @@ class InterfaceFactory(
|
|||
private val specMap: Map<InterfaceId, InterfaceSpec<*>>
|
||||
get() =
|
||||
mapOf(
|
||||
InterfaceId.BLUETOOTH to bluetoothSpec.value,
|
||||
InterfaceId.MOCK to mockSpec.value,
|
||||
InterfaceId.NOP to NopInterfaceSpec(nopInterfaceFactory),
|
||||
InterfaceId.SERIAL to serialSpec.value,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.network.radio
|
||||
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.RadioTransport
|
||||
import org.meshtastic.core.repository.RadioTransportFactory
|
||||
|
||||
/**
|
||||
* Common base class for platform [RadioTransportFactory] implementations. Handles KMP-friendly transports (BLE) while
|
||||
* delegating platform-specific ones (like TCP, USB/Serial and Mocks) to the abstract [createPlatformTransport].
|
||||
*/
|
||||
abstract class BaseRadioTransportFactory(
|
||||
protected val scanner: BleScanner,
|
||||
protected val bluetoothRepository: BluetoothRepository,
|
||||
protected val connectionFactory: BleConnectionFactory,
|
||||
protected val dispatchers: CoroutineDispatchers,
|
||||
) : RadioTransportFactory {
|
||||
|
||||
override fun isAddressValid(address: String?): Boolean {
|
||||
val spec = address?.firstOrNull() ?: return false
|
||||
return spec in
|
||||
listOf(InterfaceId.TCP.id, InterfaceId.SERIAL.id, InterfaceId.BLUETOOTH.id, InterfaceId.MOCK.id) ||
|
||||
spec == '!' ||
|
||||
isPlatformAddressValid(address)
|
||||
}
|
||||
|
||||
protected open fun isPlatformAddressValid(address: String): Boolean = false
|
||||
|
||||
override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = "${interfaceId.id}$rest"
|
||||
|
||||
override fun createTransport(address: String, service: RadioInterfaceService): RadioTransport = when {
|
||||
address.startsWith(InterfaceId.BLUETOOTH.id) -> {
|
||||
BleRadioInterface(
|
||||
serviceScope = service.serviceScope,
|
||||
scanner = scanner,
|
||||
bluetoothRepository = bluetoothRepository,
|
||||
connectionFactory = connectionFactory,
|
||||
service = service,
|
||||
address = address.removePrefix(InterfaceId.BLUETOOTH.id.toString()),
|
||||
)
|
||||
}
|
||||
address.startsWith("!") -> {
|
||||
BleRadioInterface(
|
||||
serviceScope = service.serviceScope,
|
||||
scanner = scanner,
|
||||
bluetoothRepository = bluetoothRepository,
|
||||
connectionFactory = connectionFactory,
|
||||
service = service,
|
||||
address = address.removePrefix("!"),
|
||||
)
|
||||
}
|
||||
else -> createPlatformTransport(address, service)
|
||||
}
|
||||
|
||||
/** Delegate to platform for Mock, TCP, or Serial/USB interfaces. */
|
||||
protected abstract fun createPlatformTransport(address: String, service: RadioInterfaceService): RadioTransport
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.meshtastic.core.network.radio
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.first
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -47,6 +47,7 @@ import org.meshtastic.core.common.util.nowMillis
|
|||
import org.meshtastic.core.model.RadioNotConnectedException
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.RadioTransport
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val SCAN_RETRY_COUNT = 3
|
||||
|
|
@ -70,7 +71,6 @@ private val SCAN_TIMEOUT = 5.seconds
|
|||
* @param service The [RadioInterfaceService] to use for handling radio events.
|
||||
* @param address The BLE address of the device to connect to.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
class BleRadioInterface(
|
||||
private val serviceScope: CoroutineScope,
|
||||
private val scanner: BleScanner,
|
||||
|
|
@ -94,7 +94,9 @@ class BleRadioInterface(
|
|||
}
|
||||
|
||||
private val connectionScope: CoroutineScope =
|
||||
CoroutineScope(serviceScope.coroutineContext + SupervisorJob() + exceptionHandler)
|
||||
CoroutineScope(
|
||||
serviceScope.coroutineContext + SupervisorJob(serviceScope.coroutineContext.job) + exceptionHandler,
|
||||
)
|
||||
private val bleConnection: BleConnection = connectionFactory.create(connectionScope, address)
|
||||
private val writeMutex: Mutex = Mutex()
|
||||
|
||||
|
|
@ -104,7 +106,9 @@ class BleRadioInterface(
|
|||
private var bytesReceived: Long = 0
|
||||
private var bytesSent: Long = 0
|
||||
|
||||
@Volatile private var isFullyConnected = false
|
||||
@Suppress("VolatileModifier")
|
||||
@Volatile
|
||||
private var isFullyConnected = false
|
||||
|
||||
init {
|
||||
connect()
|
||||
|
|
@ -344,10 +348,10 @@ class BleRadioInterface(
|
|||
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
|
||||
"Packets TX: $packetsSent ($bytesSent bytes)"
|
||||
}
|
||||
connectionScope.launch {
|
||||
serviceScope.launch {
|
||||
connectionScope.cancel()
|
||||
bleConnection.disconnect()
|
||||
service.onDisconnect(true)
|
||||
connectionScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue