diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c95e0dd78..b63f8d9df 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -104,19 +104,6 @@
android:enabled="true"
android:exported="false" />
-
-
-
+
+
@@ -145,6 +134,11 @@
android:host="www.meshtastic.org"
android:pathPrefix="/c/" />
+
+
+
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 823fad2e5..64edf56d7 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -13,6 +13,8 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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
@@ -427,6 +429,11 @@ class MainActivity : AppCompatActivity(), Logging,
// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel
}
+
+ if (appLinkAction == UsbManager.ACTION_USB_ACCESSORY_ATTACHED) {
+ val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
+ errormsg("Handle USB device attached! $device")
+ }
}
override fun onDestroy() {
diff --git a/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt b/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt
index 789644ad1..804c2b964 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt
@@ -6,44 +6,58 @@ import com.geeksville.android.Logging
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
+import kotlin.concurrent.thread
class SerialInterface(private val service: RadioInterfaceService, val address: String) : Logging,
IRadioInterface {
- companion object {
+ companion object : Logging {
private const val START1 = 0x94.toByte()
private const val START2 = 0xc3.toByte()
private const val MAX_TO_FROM_RADIO_SIZE = 512
+
+ fun findDrivers(context: Context): List {
+ val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
+ val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
+ val devices = drivers.map { it.device }
+ devices.forEach { d ->
+ debug("Found serial port $d")
+ }
+ return drivers
+ }
}
- private var uart: UsbSerialPort? = null
-
- private val manager: UsbManager by lazy {
- service.getSystemService(Context.USB_SERVICE) as UsbManager
- }
+ private var uart: UsbSerialPort?
+ private lateinit var reader: Thread
init {
- val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
+ val manager = service.getSystemService(Context.USB_SERVICE) as UsbManager
+ val drivers = findDrivers(this)
// Open a connection to the first available driver.
- // Open a connection to the first available driver.
- val driver: UsbSerialDriver = drivers[0]
- val connection = manager.openDevice(driver.device)
+ val device = drivers[0].device
+
+ info("Opening $device")
+ val connection = manager.openDevice(device)
if (connection == null) {
- // FIXME add UsbManager.requestPermission(driver.getDevice(), ..) handling to activity
- TODO()
+ // FIXME add UsbManager.requestPermission(device, ..) handling to activity
+ TODO("Need permissions for port")
} else {
- val port = driver.ports[0] // Most devices have just one port (port 0)
+ val port = drivers[0].ports[0] // Most devices have just one port (port 0)
port.open(connection)
port.setParameters(921600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
uart = port
+ debug("Starting serial reader thread")
// FIXME, start reading thread
+ reader =
+ thread(start = true, isDaemon = true, name = "serial reader", block = ::readerLoop)
}
}
override fun handleSendToRadio(p: ByteArray) {
+ // This method is called from a continuation and it might show up late, so check for uart being null
uart?.apply {
val header = ByteArray(4)
header[0] = START1
@@ -57,7 +71,7 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
/** Print device serial debug output somewhere */
private fun debugOut(c: Byte) {
-
+ debug("Got c: ${c.toChar()}")
}
private fun readerLoop() {
@@ -71,7 +85,7 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
var msb = 0
var lsb = 0
- while (true) { // FIXME wait for someone to ask us to exit, and catch continuation exception
+ while (uart != null) { // we run until our port gets closed
uart?.apply {
read(scratch, 0)
val c = scratch[0]
@@ -113,7 +127,8 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
}
override fun close() {
- uart?.close()
+ debug("Closing serial port")
+ uart?.close() // This will cause the reader thread to exit
uart = null
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
index ab3014b74..2cda9f26b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -2,6 +2,7 @@ package com.geeksville.mesh.ui
import android.annotation.SuppressLint
import android.app.Application
+import android.app.PendingIntent
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.BOND_BONDED
import android.bluetooth.BluetoothDevice.BOND_BONDING
@@ -11,6 +12,8 @@ import android.companion.AssociationRequest
import android.companion.BluetoothDeviceFilter
import android.companion.CompanionDeviceManager
import android.content.*
+import android.hardware.usb.UsbDevice
+import android.hardware.usb.UsbManager
import android.os.Bundle
import android.os.ParcelUuid
import android.view.LayoutInflater
@@ -32,6 +35,7 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.BluetoothInterface
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.RadioInterfaceService
+import com.geeksville.mesh.service.SerialInterface
import com.geeksville.util.anonymize
import com.geeksville.util.exceptionReporter
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -105,6 +109,9 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
override fun toString(): String {
return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize})"
}
+
+ val isBluetooth: Boolean get() = name[0] == 'x'
+ val isSerial: Boolean get() = name[0] == 's'
}
override fun onCleared() {
@@ -116,10 +123,11 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
+ private val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
+
var selectedAddress: String? = null
val errorText = object : MutableLiveData(null) {}
-
private var scanner: BluetoothLeScanner? = null
/// If this address is for a bluetooth device, return the macaddr portion, else null
@@ -231,6 +239,17 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
// Include a placeholder for "None"
addDevice(DeviceListEntry(context.getString(R.string.none), "n", true))
+ SerialInterface.findDrivers(context).forEach { d ->
+ val hasPerms = usbManager.hasPermission(d.device)
+ addDevice(
+ DeviceListEntry(
+ d.device.deviceName,
+ "s${d.device.deviceName}",
+ hasPerms
+ )
+ )
+ }
+
// filter and only accept devices that have a sw update service
val filter =
ScanFilter.Builder()
@@ -279,25 +298,70 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
changeScanSelection(activity, it.address)
return true
} else {
- // We ignore missing BT adapters, because it lets us run on the emulator
- bluetoothAdapter
- ?.getRemoteDevice(it.address)?.let { device ->
- requestBonding(activity, device) { state ->
- if (state == BOND_BONDED) {
- errorText.value = activity.getString(R.string.pairing_completed)
- changeScanSelection(
- activity,
- it.address
- )
- } else {
- errorText.value = activity.getString(R.string.pairing_failed_try_again)
- }
+ // Handle requestng USB or bluetooth permissions for the device
- // Force the GUI to redraw
- devices.value = devices.value
+ if (it.isBluetooth) {
+ // Request bonding for bluetooth
+ // We ignore missing BT adapters, because it lets us run on the emulator
+ bluetoothAdapter
+ ?.getRemoteDevice(it.address)?.let { device ->
+ requestBonding(activity, device) { state ->
+ if (state == BOND_BONDED) {
+ errorText.value = activity.getString(R.string.pairing_completed)
+ changeScanSelection(
+ activity,
+ it.address
+ )
+ } else {
+ errorText.value =
+ activity.getString(R.string.pairing_failed_try_again)
+ }
+
+ // Force the GUI to redraw
+ devices.value = devices.value
+ }
+ }
+ }
+
+ if (it.isSerial) {
+ val ACTION_USB_PERMISSION = "com.geeksville.mesh.USB_PERMISSION"
+
+ val usbReceiver = object : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (ACTION_USB_PERMISSION == intent.action) {
+
+ val device: UsbDevice? =
+ intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
+
+ if (intent.getBooleanExtra(
+ UsbManager.EXTRA_PERMISSION_GRANTED,
+ false
+ )
+ ) {
+ device?.apply {
+ info("User approved USB access")
+ changeScanSelection(activity, it.address)
+
+ // Force the GUI to redraw
+ devices.value = devices.value
+ }
+ } else {
+ errormsg("USB permission denied for device $device")
+ }
+ }
+ // We don't need to stay registered
+ activity.unregisterReceiver(this)
}
}
+ val permissionIntent =
+ PendingIntent.getBroadcast(activity, 0, Intent(ACTION_USB_PERMISSION), 0)
+ val filter = IntentFilter(ACTION_USB_PERMISSION)
+ activity.registerReceiver(usbReceiver, filter)
+ usbManager.requestPermission(device, permissionIntent)
+ }
+
return false
}
}