sforkowany z mirror/meshtastic-android
Initial step in refactoring RadioInterfaceService for dependency injection
Extracts USB device management into a `UsbRepository`. In order for `SerialInterface to gain access to this prior to the `RadioInterfaceService` being fully natively dependency injected, all `InterfaceFactory` implementations needed to be modified to accept the `UsbRepository` via argument. This will go away in a future PR. Changed `assumePermission` constant to `false` as it was preventing the request for permission from occurring, breaking serial connectivity. Minor improvement: SerialInterface re-bonding by device name is now supported.master
rodzic
26b6081e9c
commit
dd41527bbc
|
|
@ -88,6 +88,7 @@ android {
|
|||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn' ]
|
||||
}
|
||||
lint {
|
||||
abortOnError false
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ import android.content.pm.PackageManager
|
|||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.RemoteException
|
||||
import android.os.*
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
|
|
@ -23,7 +19,6 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
|
@ -44,6 +39,7 @@ import com.geeksville.mesh.model.BluetoothViewModel
|
|||
import com.geeksville.mesh.model.ChannelSet
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.mesh.service.*
|
||||
import com.geeksville.mesh.ui.*
|
||||
import com.geeksville.util.Exceptions
|
||||
|
|
@ -66,6 +62,7 @@ import kotlinx.coroutines.cancel
|
|||
import java.nio.charset.Charset
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
UI design
|
||||
|
|
@ -138,6 +135,9 @@ class MainActivity : BaseActivity(), Logging,
|
|||
private val bluetoothViewModel: BluetoothViewModel by viewModels()
|
||||
val model: UIViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
internal lateinit var usbRepository: UsbRepository
|
||||
|
||||
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
|
||||
|
||||
// private val tabIndexes = generateSequence(0) { it + 1 } FIXME, instead do withIndex or zip? to get the ids below, also stop duplicating strings
|
||||
|
|
@ -974,7 +974,7 @@ class MainActivity : BaseActivity(), Logging,
|
|||
bluetoothViewModel.enabled.observe(this) { enabled ->
|
||||
if (!enabled) {
|
||||
// Ask to start bluetooth if no USB devices are visible
|
||||
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
|
||||
val hasUSB = usbRepository.serialDevicesWithDrivers.value.isNotEmpty()
|
||||
if (!isInTestLab && !hasUSB) {
|
||||
if (hasConnectPermission()) {
|
||||
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||
|
|
@ -991,7 +991,7 @@ class MainActivity : BaseActivity(), Logging,
|
|||
errormsg("Bind of MeshService failed")
|
||||
}
|
||||
|
||||
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
|
||||
val bonded = RadioInterfaceService.getBondedDeviceAddress(this, usbRepository) != null
|
||||
if (!bonded && usbDevice == null) // we will handle USB later
|
||||
showSettingsPage()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.geeksville.mesh.repository.usb
|
||||
|
||||
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
|
||||
import com.hoho.android.usbserial.driver.ProbeTable
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import dagger.Reusable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* Creates a probe table for the USB driver. This augments the default device-to-driver
|
||||
* mappings with additional known working configurations. See this package's README for
|
||||
* more info.
|
||||
*/
|
||||
@Reusable
|
||||
class ProbeTableProvider @Inject constructor() : Provider<ProbeTable> {
|
||||
override fun get(): ProbeTable {
|
||||
return UsbSerialProber.getDefaultProbeTable().apply {
|
||||
// RAK 4631:
|
||||
addProduct(9114, 32809, CdcAcmSerialDriver::class.java)
|
||||
// LilyGo TBeam v1.1:
|
||||
addProduct(6790, 21972, CdcAcmSerialDriver::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# USB Module
|
||||
|
||||
This module provides a repository for acessing USB devices.
|
||||
|
||||
## Device Support
|
||||
|
||||
In order to be picked up, devices need to be supported by two different mechanisms:
|
||||
- Android needs to be supplied with a device filter so that it knows what devices to inform
|
||||
the app about. These are expressed as vendor and device IDs in `src/res/xml/device_filter.xml`.
|
||||
- The USB driver library also needs to have a mapping between the vendor + device IDs and the
|
||||
driver to use for communications. Many mappings are already natively supported by the driver
|
||||
but unknown devices can have manual mappings added via `ProbeTableProvider`.
|
||||
|
||||
The [Serial USB Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_usb_terminal)
|
||||
app in the Google Play Store seems to be a good app for determining both the vendor and
|
||||
device IDs as well as testing different underlying drivers.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
When granting permissions to a USB device, the Android platform remembers the user's decision.
|
||||
In order to test the permission granting logic, re-install the app. This will cause Android
|
||||
to forget previously granted permissions and will re-trigger the permission acquisition logic.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.geeksville.mesh.repository.usb
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.util.exceptionReporter
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are
|
||||
* changed.
|
||||
*/
|
||||
class UsbBroadcastReceiver @Inject constructor(
|
||||
private val usbRepository: UsbRepository
|
||||
) : BroadcastReceiver(), Logging {
|
||||
// Can be used for registering
|
||||
internal val intentFilter get() = IntentFilter().apply {
|
||||
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
val deviceName: String = intent.getParcelableExtra<UsbDevice?>(UsbManager.EXTRA_DEVICE)?.deviceName ?: "unknown"
|
||||
when (intent.action) {
|
||||
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||
debug("USB device '$deviceName' was detached")
|
||||
usbRepository.refreshState()
|
||||
}
|
||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||
debug("USB device '$deviceName' was attached")
|
||||
usbRepository.refreshState()
|
||||
}
|
||||
UsbManager.EXTRA_PERMISSION_GRANTED -> {
|
||||
debug("USB device '$deviceName' was granted permission")
|
||||
usbRepository.refreshState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.geeksville.mesh.repository.usb
|
||||
|
||||
import android.app.Application
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Repository responsible for maintaining and updating the state of USB connectivity.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Singleton
|
||||
class UsbRepository @Inject constructor(
|
||||
private val application: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val processLifecycle: Lifecycle,
|
||||
private val usbBroadcastReceiverLazy: dagger.Lazy<UsbBroadcastReceiver>,
|
||||
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
|
||||
private val usbSerialProberLazy: dagger.Lazy<UsbSerialProber>
|
||||
) : Logging {
|
||||
private val _serialDevices = MutableStateFlow(emptyMap<String, UsbDevice>())
|
||||
|
||||
@Suppress("unused") // Retained as public API
|
||||
val serialDevices = _serialDevices
|
||||
.asStateFlow()
|
||||
|
||||
val serialDevicesWithDrivers = _serialDevices
|
||||
.mapLatest { serialDevices ->
|
||||
val serialProber = usbSerialProberLazy.get()
|
||||
buildMap {
|
||||
serialDevices.forEach { (k, v) ->
|
||||
serialProber.probeDevice(v)?.let { driver ->
|
||||
put(k, driver)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
@Suppress("unused") // Retained as public API
|
||||
val serialDevicesWithPermission = _serialDevices
|
||||
.mapLatest { serialDevices ->
|
||||
usbManagerLazy.get()?.let { usbManager ->
|
||||
serialDevices.filterValues { device ->
|
||||
usbManager.hasPermission(device)
|
||||
}
|
||||
} ?: emptyMap()
|
||||
}.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
init {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
refreshStateInternal()
|
||||
usbBroadcastReceiverLazy.get().let { receiver ->
|
||||
application.registerReceiver(receiver, receiver.intentFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshState() {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
refreshStateInternal()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshStateInternal() = withContext(dispatchers.default) {
|
||||
_serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.geeksville.mesh.repository.usb
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.hardware.usb.UsbManager
|
||||
import com.hoho.android.usbserial.driver.ProbeTable
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface UsbRepositoryModule {
|
||||
companion object {
|
||||
@Provides
|
||||
fun provideUsbManager(application: Application): UsbManager? =
|
||||
application.getSystemService(Context.USB_SERVICE) as UsbManager?
|
||||
|
||||
@Provides
|
||||
fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
|
||||
|
||||
@Provides
|
||||
fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,12 @@ import android.bluetooth.BluetoothAdapter
|
|||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattService
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.util.anonymize
|
||||
import com.geeksville.util.exceptionReporter
|
||||
import com.geeksville.util.ignoreException
|
||||
|
|
@ -85,6 +85,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
companion object : Logging, InterfaceFactory('x') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
|
||||
rest: String
|
||||
): IRadioInterface = BluetoothInterface(service, rest)
|
||||
|
||||
|
|
@ -111,7 +112,11 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
@SuppressLint("NewApi", "MissingPermission")
|
||||
override fun addressValid(context: Context, rest: String): Boolean {
|
||||
override fun addressValid(
|
||||
context: Context,
|
||||
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
|
||||
rest: String
|
||||
): Boolean {
|
||||
/* val allPaired = if (hasCompanionDeviceApi(context)) {
|
||||
val deviceManager: CompanionDeviceManager by lazy {
|
||||
context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.Context
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
|
||||
/**
|
||||
* A base class for the singleton factories that make interfaces. One instance per interface type
|
||||
|
|
@ -16,8 +17,8 @@ abstract class InterfaceFactory(val prefix: Char) {
|
|||
factories[prefix] = this
|
||||
}
|
||||
|
||||
abstract fun createInterface(service: RadioInterfaceService, rest: String): IRadioInterface
|
||||
abstract fun createInterface(service: RadioInterfaceService, usbRepository: UsbRepository, rest: String): IRadioInterface
|
||||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
open fun addressValid(context: Context, rest: String): Boolean = true
|
||||
open fun addressValid(context: Context, usbRepository: UsbRepository, rest: String): Boolean = true
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import com.geeksville.mesh.android.hasBackgroundPermission
|
|||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
|
||||
import com.geeksville.util.*
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
|
|
@ -56,6 +57,9 @@ class MeshService : Service(), Logging {
|
|||
@Inject
|
||||
lateinit var packetRepository: Lazy<PacketRepository>
|
||||
|
||||
@Inject
|
||||
lateinit var usbRepository: Lazy<UsbRepository>
|
||||
|
||||
companion object : Logging {
|
||||
|
||||
/// Intents broadcast by MeshService
|
||||
|
|
@ -306,7 +310,7 @@ class MeshService : Service(), Logging {
|
|||
* tell android not to kill us
|
||||
*/
|
||||
private fun startForeground() {
|
||||
val a = RadioInterfaceService.getBondedDeviceAddress(this)
|
||||
val a = RadioInterfaceService.getBondedDeviceAddress(this, usbRepository.get())
|
||||
val wantForeground = a != null && a != "n"
|
||||
|
||||
info("Requesting foreground service=$wantForeground")
|
||||
|
|
@ -1337,7 +1341,7 @@ class MeshService : Service(), Logging {
|
|||
private fun regenMyNodeInfo() {
|
||||
val myInfo = rawMyNodeInfo
|
||||
if (myInfo != null) {
|
||||
val a = RadioInterfaceService.getBondedDeviceAddress(this)
|
||||
val a = RadioInterfaceService.getBondedDeviceAddress(this, usbRepository.get())
|
||||
val isBluetoothInterface = a != null && a.startsWith("x")
|
||||
|
||||
val nodeNum =
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.geeksville.android.GeeksvilleApplication
|
|||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.model.getInitials
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.google.protobuf.ByteString
|
||||
import okhttp3.internal.toHexString
|
||||
|
||||
|
|
@ -14,10 +15,15 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
|
|||
companion object : Logging, InterfaceFactory('m') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
|
||||
rest: String
|
||||
): IRadioInterface = MockInterface(service)
|
||||
|
||||
override fun addressValid(context: Context, rest: String): Boolean =
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
|
||||
class NopInterface : IRadioInterface {
|
||||
companion object : Logging, InterfaceFactory('n') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
|
||||
rest: String
|
||||
): IRadioInterface = NopInterface()
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ import com.geeksville.android.Logging
|
|||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.IRadioInterfaceService
|
||||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.util.anonymize
|
||||
import com.geeksville.util.ignoreException
|
||||
import com.geeksville.util.toRemoteExceptions
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
|
@ -50,6 +50,9 @@ class RadioInterfaceService : Service(), Logging {
|
|||
@Inject
|
||||
lateinit var bluetoothRepository: BluetoothRepository
|
||||
|
||||
@Inject
|
||||
lateinit var usbRepository: UsbRepository
|
||||
|
||||
companion object : Logging {
|
||||
/**
|
||||
* The RECEIVED_FROMRADIO
|
||||
|
|
@ -95,13 +98,13 @@ class RadioInterfaceService : Service(), Logging {
|
|||
* and t is an interface specific address (macaddr or a device path)
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun getDeviceAddress(context: Context): String? {
|
||||
fun getDeviceAddress(context: Context, usbRepository: UsbRepository): String? {
|
||||
// If the user has unpaired our device, treat things as if we don't have one
|
||||
val prefs = getPrefs(context)
|
||||
var address = prefs.getString(DEVADDR_KEY, null)
|
||||
|
||||
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the user
|
||||
if (address == null && MockInterface.addressValid(context, ""))
|
||||
if (address == null && MockInterface.addressValid(context, usbRepository, ""))
|
||||
address = MockInterface.prefix.toString()
|
||||
|
||||
return address
|
||||
|
|
@ -115,15 +118,15 @@ class RadioInterfaceService : Service(), Logging {
|
|||
* and t is an interface specific address (macaddr or a device path)
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun getBondedDeviceAddress(context: Context): String? {
|
||||
fun getBondedDeviceAddress(context: Context, usbRepository: UsbRepository): String? {
|
||||
// If the user has unpaired our device, treat things as if we don't have one
|
||||
val address = getDeviceAddress(context)
|
||||
val address = getDeviceAddress(context, usbRepository)
|
||||
|
||||
/// Interfaces can filter addresses to indicate that address is no longer acceptable
|
||||
if (address != null) {
|
||||
val c = address[0]
|
||||
val rest = address.substring(1)
|
||||
val isValid = InterfaceFactory.getFactory(c)?.addressValid(context, rest) ?: false
|
||||
val isValid = InterfaceFactory.getFactory(c)?.addressValid(context, usbRepository, rest) ?: false
|
||||
if (!isValid)
|
||||
return null
|
||||
}
|
||||
|
|
@ -238,7 +241,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
if (radioIf !is NopInterface)
|
||||
warn("Can't start interface - $radioIf is already running")
|
||||
else {
|
||||
val address = getBondedDeviceAddress(this)
|
||||
val address = getBondedDeviceAddress(this, usbRepository)
|
||||
if (address == null)
|
||||
warn("No bonded mesh radio, can't start interface")
|
||||
else {
|
||||
|
|
@ -253,7 +256,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
val c = address[0]
|
||||
val rest = address.substring(1)
|
||||
radioIf =
|
||||
InterfaceFactory.getFactory(c)?.createInterface(this, rest) ?: NopInterface()
|
||||
InterfaceFactory.getFactory(c)?.createInterface(this, usbRepository, rest) ?: NopInterface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -287,7 +290,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
*/
|
||||
@SuppressLint("NewApi")
|
||||
private fun setBondedDeviceAddress(address: String?): Boolean {
|
||||
return if (getBondedDeviceAddress(this) == address && isStarted) {
|
||||
return if (getBondedDeviceAddress(this, usbRepository) == address && isStarted) {
|
||||
warn("Ignoring setBondedDevice ${address.anonymize}, because we are already using that device")
|
||||
false
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.android.usbManager
|
||||
import com.geeksville.util.exceptionReporter
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.util.ignoreException
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||
import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import com.hoho.android.usbserial.util.SerialInputOutputManager
|
||||
|
||||
|
||||
/**
|
||||
* An interface that assumes we are talking to a meshtastic device via USB serial
|
||||
*/
|
||||
class SerialInterface(service: RadioInterfaceService, private val address: String) :
|
||||
class SerialInterface(
|
||||
service: RadioInterfaceService,
|
||||
private val usbRepository: UsbRepository,
|
||||
private val address: String) :
|
||||
StreamInterface(service), Logging, SerialInputOutputManager.Listener {
|
||||
companion object : Logging, InterfaceFactory('s') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
usbRepository: UsbRepository,
|
||||
rest: String
|
||||
): IRadioInterface = SerialInterface(service, rest)
|
||||
): IRadioInterface = SerialInterface(service, usbRepository, rest)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
|
|
@ -36,77 +35,44 @@ class SerialInterface(service: RadioInterfaceService, private val address: Strin
|
|||
* we should never ask for USB permissions ourselves, instead we should rely on the external dialog printed by the system. If
|
||||
* we do that the system will remember we have accesss
|
||||
*/
|
||||
const val assumePermission = true
|
||||
const val assumePermission = false
|
||||
|
||||
fun toInterfaceName(deviceName: String) = "s$deviceName"
|
||||
|
||||
fun findDrivers(context: Context): List<UsbSerialDriver> {
|
||||
val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(context.usbManager)
|
||||
val devices = drivers.map { it.device }
|
||||
devices.forEach { d ->
|
||||
debug("Found serial port ${d.deviceName}")
|
||||
override fun addressValid(
|
||||
context: Context,
|
||||
usbRepository: UsbRepository,
|
||||
rest: String
|
||||
): Boolean {
|
||||
usbRepository.serialDevicesWithDrivers.value.filterValues {
|
||||
assumePermission || context.usbManager.hasPermission(it.device)
|
||||
}
|
||||
return drivers
|
||||
}
|
||||
|
||||
override fun addressValid(context: Context, rest: String): Boolean {
|
||||
findSerial(context, rest)?.let { d ->
|
||||
findSerial(usbRepository, rest)?.let { d ->
|
||||
return assumePermission || context.usbManager.hasPermission(d.device)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun findSerial(context: Context, rest: String): UsbSerialDriver? {
|
||||
val drivers = findDrivers(context)
|
||||
|
||||
return if (drivers.isEmpty())
|
||||
null
|
||||
else // Open a connection to the first available driver.
|
||||
drivers[0] // FIXME, instead we should find by name
|
||||
private fun findSerial(usbRepository: UsbRepository, rest: String): UsbSerialDriver? {
|
||||
val deviceMap = usbRepository.serialDevicesWithDrivers.value
|
||||
deviceMap.forEach { (path, _) ->
|
||||
debug("Found serial port: $path")
|
||||
}
|
||||
return if (deviceMap.containsKey(rest)) {
|
||||
deviceMap[rest]!!
|
||||
} else {
|
||||
deviceMap.map { (_, driver) -> driver }.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var uart: UsbSerialDriver? = null
|
||||
private var ioManager: SerialInputOutputManager? = null
|
||||
|
||||
private var usbReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
|
||||
if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
|
||||
debug("A USB device was detached")
|
||||
val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
|
||||
if (uart?.device == device)
|
||||
onDeviceDisconnect(true)
|
||||
}
|
||||
|
||||
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
|
||||
debug("attaching USB")
|
||||
val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
|
||||
if (assumePermission || context.usbManager.hasPermission(device)) {
|
||||
// reinit the port from scratch and reopen
|
||||
onDeviceDisconnect(true)
|
||||
connect()
|
||||
} else {
|
||||
warn("We don't have permissions for this USB device")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
|
||||
service.registerReceiver(usbReceiver, filter)
|
||||
|
||||
connect()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
service.unregisterReceiver(usbReceiver)
|
||||
super.close()
|
||||
}
|
||||
|
||||
/** Tell MeshService our device has gone away, but wait for it to come back
|
||||
*
|
||||
* @param waitForStopped if true we should wait for the manager to finish - must be false if called from inside the manager callbacks
|
||||
|
|
@ -145,7 +111,7 @@ class SerialInterface(service: RadioInterfaceService, private val address: Strin
|
|||
|
||||
override fun connect() {
|
||||
val manager = service.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
val device = findSerial(service, address)
|
||||
val device = findSerial(usbRepository, address)
|
||||
|
||||
if (device != null) {
|
||||
info("Opening $device")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.util.Exceptions
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.IOException
|
||||
|
|
@ -18,6 +19,7 @@ class TCPInterface(service: RadioInterfaceService, private val address: String)
|
|||
companion object : Logging, InterfaceFactory('t') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
|
||||
rest: String
|
||||
): IRadioInterface = TCPInterface(service, rest)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import com.geeksville.mesh.android.*
|
|||
import com.geeksville.mesh.databinding.SettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.mesh.service.*
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
|
||||
|
|
@ -50,10 +51,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
object SLogging : Logging
|
||||
|
||||
|
|
@ -108,7 +109,11 @@ private fun requestBonding(
|
|||
}
|
||||
}
|
||||
|
||||
class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||
@HiltViewModel
|
||||
class BTScanModel @Inject constructor(
|
||||
private val usbRepository: UsbRepository,
|
||||
app: Application
|
||||
) : AndroidViewModel(app), Logging {
|
||||
|
||||
private val context: Context get() = getApplication<Application>().applicationContext
|
||||
|
||||
|
|
@ -243,9 +248,9 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
* returns true if we could start scanning, false otherwise
|
||||
*/
|
||||
fun setupScan(): Boolean {
|
||||
selectedAddress = RadioInterfaceService.getDeviceAddress(context)
|
||||
selectedAddress = RadioInterfaceService.getDeviceAddress(context, usbRepository)
|
||||
|
||||
return if (bluetoothAdapter == null || MockInterface.addressValid(context, "")) {
|
||||
return if (bluetoothAdapter == null || MockInterface.addressValid(context, usbRepository, "")) {
|
||||
warn("No bluetooth adapter. Running under emulation?")
|
||||
|
||||
val testnodes = listOf(
|
||||
|
|
@ -270,11 +275,11 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
|
||||
true
|
||||
} else {
|
||||
val usbDrivers = SerialInterface.findDrivers(context)
|
||||
|
||||
/* model.bluetoothEnabled.value */
|
||||
|
||||
if (bluetoothAdapter.bluetoothLeScanner == null && usbDrivers.isEmpty()) {
|
||||
val serialDevices by lazy {
|
||||
usbRepository.serialDevicesWithDrivers.value
|
||||
}
|
||||
if (bluetoothAdapter.bluetoothLeScanner == null && serialDevices.isEmpty()) {
|
||||
errorText.value =
|
||||
context.getString(R.string.requires_bluetooth)
|
||||
|
||||
|
|
@ -288,10 +293,8 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
// Include a placeholder for "None"
|
||||
addDevice(DeviceListEntry(context.getString(R.string.none), "n", true))
|
||||
|
||||
usbDrivers.forEach { d ->
|
||||
addDevice(
|
||||
USBDeviceListEntry(usbManager, d)
|
||||
)
|
||||
serialDevices.forEach { (_, d) ->
|
||||
addDevice(USBDeviceListEntry(usbManager, d))
|
||||
}
|
||||
} else {
|
||||
debug("scan already running")
|
||||
|
|
@ -454,7 +457,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
// FIXME - move this into a standard GUI helper class
|
||||
private val guiJob = Job()
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main + guiJob)
|
||||
|
||||
@Inject
|
||||
internal lateinit var usbRepository: UsbRepository
|
||||
|
||||
private val hasCompanionDeviceApi: Boolean by lazy {
|
||||
BluetoothInterface.hasCompanionDeviceApi(requireContext())
|
||||
|
|
@ -823,9 +828,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
// get rid of the warning text once at least one device is paired.
|
||||
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
|
||||
val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext())
|
||||
val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext(), usbRepository)
|
||||
|
||||
if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) {
|
||||
if (curRadio != null && !MockInterface.addressValid(requireContext(), usbRepository, "")) {
|
||||
binding.warningNotPaired.visibility = View.GONE
|
||||
// binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio)
|
||||
} else if (bluetoothViewModel.enabled.value == true){
|
||||
|
|
@ -1041,7 +1046,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
myActivity.registerReceiver(updateProgressReceiver, updateProgressFilter)
|
||||
|
||||
// Keep reminding user BLE is still off
|
||||
val hasUSB = SerialInterface.findDrivers(myActivity).isNotEmpty()
|
||||
val hasUSB = usbRepository.serialDevicesWithDrivers.value.isNotEmpty()
|
||||
if (!hasUSB) {
|
||||
// Warn user if BLE is disabled
|
||||
if (scanModel.bluetoothAdapter?.isEnabled != true) {
|
||||
|
|
|
|||
|
|
@ -55,4 +55,5 @@
|
|||
<usb-device
|
||||
vendor-id="3368"
|
||||
product-id="516" /> <!-- 0x0d28 / 0x0204: ARM mbed -->
|
||||
<usb-device vendor-id="9114" /> <!-- 0x239A RAK Wireless -->
|
||||
</resources>
|
||||
Ładowanie…
Reference in New Issue