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