Skip to content

Commit

Permalink
Continued work on #369: Inject radio interface implementations (#481)
Browse files Browse the repository at this point in the history
This required creation of new interfaces in order to break the
static coupling.  This also allowed for the removal of some plumbing
of dependencies of these implementations since they are now directly
injected.
  • Loading branch information
mcumings authored Oct 24, 2023
1 parent 1213762 commit a7b0d70
Show file tree
Hide file tree
Showing 25 changed files with 370 additions and 220 deletions.
12 changes: 8 additions & 4 deletions app/src/main/java/com/geeksville/mesh/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.primaryChannel
import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.repository.radio.BluetoothInterface
import com.geeksville.mesh.repository.radio.SerialInterface
import com.geeksville.mesh.repository.radio.InterfaceId
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.*
import com.geeksville.mesh.ui.*
import com.geeksville.mesh.ui.map.MapFragment
Expand Down Expand Up @@ -121,6 +122,9 @@ class MainActivity : AppCompatActivity(), Logging {
@Inject
internal lateinit var serviceRepository: ServiceRepository

@Inject
internal lateinit var radioInterfaceService: RadioInterfaceService

private val bluetoothPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
if (result.entries.all { it.value }) {
Expand Down Expand Up @@ -462,9 +466,9 @@ class MainActivity : AppCompatActivity(), Logging {
try {
usbDevice?.let { usb ->
debug("Switching to USB radio ${usb.deviceName}")
service.setDeviceAddress(SerialInterface.toInterfaceName(usb.deviceName))
usbDevice =
null // Only switch once - thereafter it should be stored in settings
val address = radioInterfaceService.toInterfaceAddress(InterfaceId.SERIAL, usb.deviceName)
service.setDeviceAddress(address)
usbDevice = null // Only switch once - thereafter it should be stored in settings
}

val connectionState =
Expand Down
17 changes: 10 additions & 7 deletions app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.android.*
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.nsd.NsdRepository
import com.geeksville.mesh.repository.radio.MockInterface
import com.geeksville.mesh.repository.radio.InterfaceId
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.SerialInterface
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.util.anonymize
import com.hoho.android.usbserial.driver.UsbSerialDriver
Expand Down Expand Up @@ -83,10 +82,14 @@ class BTScanModel @Inject constructor(
device.bondState == BluetoothDevice.BOND_BONDED
)

class USBDeviceListEntry(usbManager: UsbManager, val usb: UsbSerialDriver) : DeviceListEntry(
class USBDeviceListEntry(
radioInterfaceService: RadioInterfaceService,
usbManager: UsbManager,
val usb: UsbSerialDriver,
) : DeviceListEntry(
usb.device.deviceName,
SerialInterface.toInterfaceName(usb.device.deviceName),
SerialInterface.assumePermission || usbManager.hasPermission(usb.device)
radioInterfaceService.toInterfaceAddress(InterfaceId.SERIAL, usb.device.deviceName),
usbManager.hasPermission(usb.device),
)

class TCPDeviceListEntry(val service: NsdServiceInfo) : DeviceListEntry(
Expand Down Expand Up @@ -177,7 +180,7 @@ class BTScanModel @Inject constructor(
private fun setupScan(): Boolean {
selectedAddress = radioInterfaceService.getDeviceAddress()

return if (MockInterface.addressValid(context, usbRepository, "")) {
return if (radioInterfaceService.isAddressValid(radioInterfaceService.mockInterfaceAddress)) {
warn("Running under emulator/test lab")

val testnodes = listOf(
Expand Down Expand Up @@ -215,7 +218,7 @@ class BTScanModel @Inject constructor(
}

usbDevices.value?.forEach { (_, d) ->
addDevice(USBDeviceListEntry(context.usbManager, d))
addDevice(USBDeviceListEntry(radioInterfaceService, context.usbManager, d))
}

devices.value = newDevs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.geeksville.mesh.repository.radio

import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.content.Context
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.bluetoothManager
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.service.*
import com.geeksville.mesh.util.anonymize
import com.geeksville.mesh.util.exceptionReporter
import com.geeksville.mesh.util.ignoreException
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
Expand Down Expand Up @@ -76,25 +77,15 @@ A variable keepAllPackets, if set to true will suppress this behavior and instea
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
*/
class BluetoothInterface(
val context: Context,
val service: RadioInterfaceService,
val address: String) : IRadioInterface,
Logging {

companion object : Logging, InterfaceFactory('x') {
override fun createInterface(
context: Context,
service: RadioInterfaceService,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): IRadioInterface = BluetoothInterface(context, service, rest)

init {
registerFactory()
}

/// this service UUID is publically visible for scanning
class BluetoothInterface @AssistedInject constructor(
context: Application,
bluetoothAdapter: dagger.Lazy<BluetoothAdapter?>,
private val service: RadioInterfaceService,
@Assisted val address: String,
) : IRadioInterface, Logging {

companion object {
/// this service UUID is publicly visible for scanning
val BTM_SERVICE_UUID: UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")

var invalidVersion = false
Expand All @@ -108,22 +99,6 @@ class BluetoothInterface(
val BTM_FROMNUM_CHARACTER: UUID =
UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453")

/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(
context: Context,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): Boolean {
/// Get our bluetooth adapter (should always succeed except on emulator
val allPaired = context.bluetoothManager?.adapter?.bondedDevices.orEmpty()
.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
warn("Ignoring stale bond to ${rest.anonymize}")
false
} else
true
}

/**
* this is created in onCreate()
* We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case
Expand Down Expand Up @@ -161,7 +136,7 @@ class BluetoothInterface(
init {
// Note: this call does no comms, it just creates the device object (even if the
// device is off/not connected)
val device = context.bluetoothManager?.adapter?.getRemoteDevice(address)
val device = bluetoothAdapter.get()?.getRemoteDevice(address)
if (device != null) {
info("Creating radio interface service. device=${address.anonymize}")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.geeksville.mesh.repository.radio

import dagger.assisted.AssistedFactory

/**
* Factory for creating `BluetoothInterface` instances.
*/
@AssistedFactory
interface BluetoothInterfaceFactory : InterfaceFactorySpi<BluetoothInterface>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.geeksville.mesh.repository.radio

import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.anonymize
import javax.inject.Inject

/**
* Bluetooth backend implementation.
*/
class BluetoothInterfaceSpec @Inject constructor(
private val factory: BluetoothInterfaceFactory,
private val bluetoothAdapter: dagger.Lazy<BluetoothAdapter?>

): InterfaceSpec<BluetoothInterface>, Logging {
override fun createInterface(rest: String): BluetoothInterface {
return factory.create(rest)
}

/** Return true if this address is still acceptable. For BLE that means, still bonded */
@SuppressLint("MissingPermission")
override fun addressValid(rest: String): Boolean {
val allPaired = bluetoothAdapter.get()?.bondedDevices.orEmpty()
.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
warn("Ignoring stale bond to ${rest.anonymize}")
false
} else
true
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
package com.geeksville.mesh.repository.radio

import android.content.Context
import com.geeksville.mesh.repository.usb.UsbRepository
import javax.inject.Inject
import javax.inject.Provider

/**
* A base class for the singleton factories that make interfaces. One instance per interface type
* Entry point for create radio backend instances given a specific address.
*
* This class is responsible for building and dissecting radio addresses based upon
* their interface type and the "rest" of the address (which varies per implementation).
*/
abstract class InterfaceFactory(val prefix: Char) {
companion object {
private val factories = mutableMapOf<Char, InterfaceFactory>()
class InterfaceFactory @Inject constructor(
private val nopInterfaceFactory: NopInterfaceFactory,
private val specMap: Map<InterfaceId, @JvmSuppressWildcards Provider<InterfaceSpec<*>>>
) {
internal val nopInterface by lazy {
nopInterfaceFactory.create("")
}

fun getFactory(l: Char) = factories.get(l)
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String {
return "${interfaceId.id}$rest"
}

protected fun registerFactory() {
factories[prefix] = this
fun createInterface(address: String): IRadioInterface {
val (spec, rest) = splitAddress(address)
return spec?.createInterface(rest) ?: nopInterface
}

abstract fun createInterface(context: Context, service: RadioInterfaceService, usbRepository: UsbRepository, rest: String): IRadioInterface
fun addressValid(address: String?): Boolean {
return address?.let {
val (spec, rest) = splitAddress(it)
spec?.addressValid(rest)
} ?: false
}

/** Return true if this address is still acceptable. For BLE that means, still bonded */
open fun addressValid(context: Context, usbRepository: UsbRepository, rest: String): Boolean = true
private fun splitAddress(address: String): Pair<InterfaceSpec<*>?, String> {
val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it]?.get() }
val rest = address.substring(1)
return Pair(c, rest)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.geeksville.mesh.repository.radio

/**
* Radio interface factory service provider interface. Each radio backend implementation needs
* to have a factory to create new instances. These instances are specific to a particular
* address. This interface defines a common API across all radio interfaces for obtaining
* implementation instances.
*
* This is primarily used in conjunction with Dagger assisted injection for each backend
* interface type.
*/
interface InterfaceFactorySpi<T: IRadioInterface> {
fun create(rest: String): T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.geeksville.mesh.repository.radio

/**
* Address identifiers for all supported radio backend implementations.
*/
enum class InterfaceId(val id: Char) {
BLUETOOTH('x'),
MOCK('m'),
NOP('n'),
SERIAL('s'),
TCP('t');

companion object {
fun forIdChar(id: Char): InterfaceId? {
return values().firstOrNull { it.id == id }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.geeksville.mesh.repository.radio

import dagger.MapKey

/**
* Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key.
*/
@MapKey
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
annotation class InterfaceMapKey(val value: InterfaceId)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.geeksville.mesh.repository.radio

/**
* This interface defines the contract that all radio backend implementations must adhere to.
*/
interface InterfaceSpec<T : IRadioInterface> {
fun createInterface(rest: String): T

/** Return true if this address is still acceptable. For BLE that means, still bonded */
fun addressValid(rest: String): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,17 @@
package com.geeksville.mesh.repository.radio

import android.content.Context
import com.geeksville.mesh.android.BuildUtils
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.*
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.repository.usb.UsbRepository
import com.google.protobuf.ByteString
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject

/** A simulated interface that is used for testing in the simulator */
class MockInterface(private val service: RadioInterfaceService) : Logging, IRadioInterface {
companion object : Logging, InterfaceFactory('m') {
override fun createInterface(
context: Context,
service: RadioInterfaceService,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): IRadioInterface = MockInterface(service)

override fun addressValid(
context: Context,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): Boolean =
BuildUtils.isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab)

init {
registerFactory()
}
}

class MockInterface @AssistedInject constructor(
private val service: RadioInterfaceService,
@Assisted val address: String
) : Logging, IRadioInterface {
private var messageCount = 50

// an infinite sequence of ints
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.geeksville.mesh.repository.radio

import dagger.assisted.AssistedFactory

/**
* Factory for creating `MockInterface` instances.
*/
@AssistedFactory
interface MockInterfaceFactory : InterfaceFactorySpi<MockInterface>
Loading

0 comments on commit a7b0d70

Please sign in to comment.