From 15f0e3bd5b6ed0b9a4389eb472ca40c681328245 Mon Sep 17 00:00:00 2001 From: Mike Cumings Date: Fri, 8 Apr 2022 15:26:20 -0700 Subject: [PATCH] Extract USB serial communication into USB repository This separates the USB serial connectivity implementation out of the radio service. `SerialInterface` now only deals with radio service concerns. --- .../mesh/repository/usb/SerialConnection.kt | 26 ++++ .../repository/usb/SerialConnectionImpl.kt | 91 ++++++++++++ .../usb/SerialConnectionListener.kt | 27 ++++ .../mesh/repository/usb/UsbRepository.kt | 10 ++ .../mesh/service/SerialInterface.kt | 129 +++++------------- .../mesh/service/StreamInterface.kt | 2 +- 6 files changed, 191 insertions(+), 94 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnection.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionListener.kt diff --git a/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnection.kt b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnection.kt new file mode 100644 index 000000000..906d86dee --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnection.kt @@ -0,0 +1,26 @@ +package com.geeksville.mesh.repository.usb + +/** + * USB serial connection. + */ +interface SerialConnection : AutoCloseable { + /** + * Called to initiate the serial connection. + */ + fun connect() + + /** + * Send data (asynchronously) to the serial device. If the connection is not presently + * established then the data provided is ignored / dropped. + */ + fun sendBytes(bytes: ByteArray) + + /** + * Close the USB serial connection. + * + * @param waitForStopped if true, waits for the connection to terminate before returning + */ + fun close(waitForStopped: Boolean) + + override fun close() +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt new file mode 100644 index 000000000..bb625485e --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionImpl.kt @@ -0,0 +1,91 @@ +package com.geeksville.mesh.repository.usb + +import android.hardware.usb.UsbManager +import com.geeksville.android.Logging +import com.geeksville.util.ignoreException +import com.hoho.android.usbserial.driver.UsbSerialDriver +import com.hoho.android.usbserial.driver.UsbSerialPort +import com.hoho.android.usbserial.util.SerialInputOutputManager +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +internal class SerialConnectionImpl( + private val usbManagerLazy: dagger.Lazy, + private val device: UsbSerialDriver, + private val listener: SerialConnectionListener +) : SerialConnection, Logging { + private val port = device.ports[0] // Most devices have just one port (port 0) + private val closedLatch = CountDownLatch(1) + private val closed = AtomicBoolean(false) + private val ioRef = AtomicReference() + + override fun sendBytes(bytes: ByteArray) { + ioRef.get()?.let { + debug("writing ${bytes.size} byte(s)") + it.writeAsync(bytes) + } + } + + override fun close(waitForStopped: Boolean) { + ignoreException { + if (closed.compareAndSet(false, true)) { + ioRef.get()?.stop() + port.close() // This will cause the reader thread to exit + } + + // Allow a short amount of time for the manager to quit (so the port can be cleanly closed) + if (waitForStopped) { + debug("Waiting for USB manager to stop...") + closedLatch.await(1, TimeUnit.SECONDS) + } + } + } + + override fun close() { + close(true) + } + + override fun connect() { + // We shouldn't be able to get this far without a USB subsystem so explode if that isn't true + val usbManager = usbManagerLazy.get()!! + + val usbDeviceConnection = usbManager.openDevice(device.device) + if (usbDeviceConnection == null) { + listener.onMissingPermission() + closed.set(true) + return + } + + port.open(usbDeviceConnection) + port.setParameters(921600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE) + + debug("Starting serial reader thread") + val io = SerialInputOutputManager(port, object : SerialInputOutputManager.Listener { + override fun onNewData(data: ByteArray) { + listener.onDataReceived(data) + } + + override fun onRunError(e: Exception?) { + closed.set(true) + ignoreException { + port.close() + } + closedLatch.countDown() + listener.onDisconnected(e) + } + }).apply { + readTimeout = 200 // To save battery we only timeout ever so often + ioRef.set(this) + } + + Thread(io).apply { + isDaemon = true + priority = Thread.MAX_PRIORITY + name = "serial reader" + }.start() // No need to keep reference to thread around, we quit by asking the ioManager to quit + + listener.onConnected() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionListener.kt b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionListener.kt new file mode 100644 index 000000000..38266c342 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/usb/SerialConnectionListener.kt @@ -0,0 +1,27 @@ +package com.geeksville.mesh.repository.usb + +/** + * Callbacks indicating state changes in the USB serial connection. + */ +interface SerialConnectionListener { + /** + * Unable to initiate the connection due to missing permissions. This is a terminal + * state. + */ + fun onMissingPermission() {} + + /** + * Called when a connection has been established. + */ + fun onConnected() {} + + /** + * Called when serial data is received. + */ + fun onDataReceived(bytes: ByteArray) {} + + /** + * Called when the connection has been terminated. + */ + fun onDisconnected(thrown: Exception?) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt index 94fc50a9d..1d4cbc6f8 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/usb/UsbRepository.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.geeksville.android.Logging import com.geeksville.mesh.CoroutineDispatchers +import com.hoho.android.usbserial.driver.UsbSerialDriver import com.hoho.android.usbserial.driver.UsbSerialProber import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -34,6 +35,7 @@ class UsbRepository @Inject constructor( val serialDevices = _serialDevices .asStateFlow() + @Suppress("unused") // Retained as public API val serialDevicesWithDrivers = _serialDevices .mapLatest { serialDevices -> val serialProber = usbSerialProberLazy.get() @@ -65,6 +67,14 @@ class UsbRepository @Inject constructor( } } + /** + * Creates a USB serial connection to the specified USB device. State changes and data arrival + * result in async callbacks on the supplied listener. + */ + fun createSerialConnection(device: UsbSerialDriver, listener: SerialConnectionListener) : SerialConnection { + return SerialConnectionImpl(usbManagerLazy, device, listener) + } + fun refreshState() { processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() 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 91aa30837..6e347e815 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt @@ -1,15 +1,13 @@ package com.geeksville.mesh.service import android.content.Context -import android.hardware.usb.UsbManager import com.geeksville.android.Logging import com.geeksville.mesh.android.usbManager +import com.geeksville.mesh.repository.usb.SerialConnection +import com.geeksville.mesh.repository.usb.SerialConnectionListener 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.util.SerialInputOutputManager - +import java.util.concurrent.atomic.AtomicReference /** * An interface that assumes we are talking to a meshtastic device via USB serial @@ -18,7 +16,7 @@ class SerialInterface( service: RadioInterfaceService, private val usbRepository: UsbRepository, private val address: String) : - StreamInterface(service), Logging, SerialInputOutputManager.Listener { + StreamInterface(service), Logging { companion object : Logging, InterfaceFactory('s') { override fun createInterface( service: RadioInterfaceService, @@ -55,9 +53,6 @@ class SerialInterface( 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 { @@ -66,105 +61,53 @@ class SerialInterface( } } - private var uart: UsbSerialDriver? = null - private var ioManager: SerialInputOutputManager? = null + private var connRef = AtomicReference() init { connect() } - /** 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 - * */ override fun onDeviceDisconnect(waitForStopped: Boolean) { - ignoreException { - ioManager?.let { - debug("USB device disconnected, but it might come back") - it.stop() - - // Allow a short amount of time for the manager to quit (so the port can be cleanly closed) - if (waitForStopped) { - val msecSleep = 50L - var numTries = 1000 / msecSleep - while (it.state != SerialInputOutputManager.State.STOPPED && numTries > 0) { - debug("Waiting for USB manager to stop...") - Thread.sleep(msecSleep) - numTries -= 1 - } - } - - ioManager = null - } - } - - ignoreException { - uart?.apply { - ports[0].close() // This will cause the reader thread to exit - - uart = null - } - } - + connRef.get()?.close(waitForStopped) super.onDeviceDisconnect(waitForStopped) } override fun connect() { - val manager = service.getSystemService(Context.USB_SERVICE) as UsbManager val device = findSerial(usbRepository, address) - - if (device != null) { - info("Opening $device") - val connection = - manager.openDevice(device.device) // This can fail with "Control Transfer failed" if port was aleady open - if (connection == null) { - // FIXME add UsbManager.requestPermission(device, ..) handling to activity - errormsg("Need permissions for port") - } else { - val port = device.ports[0] // Most devices have just one port (port 0) - - port.open(connection) - port.setParameters(921600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE) - uart = device - - debug("Starting serial reader thread") - val io = SerialInputOutputManager(port, this) - io.readTimeout = 200 // To save battery we only timeout ever so often - ioManager = io - - val thread = Thread(io) - thread.isDaemon = true - thread.priority = Thread.MAX_PRIORITY - thread.name = "serial reader" - thread.start() // No need to keep reference to thread around, we quit by asking the ioManager to quit - - // Now tell clients they can (finally use the api) - super.connect() - } - } else { + if (device == null) { errormsg("Can't find device") + } else { + info("Opening $device") + val onConnect: () -> Unit = { super.connect() } + usbRepository.createSerialConnection(device, object : SerialConnectionListener { + override fun onMissingPermission() { + errormsg("Need permissions for port") + } + + override fun onConnected() { + onConnect.invoke() + } + + override fun onDataReceived(bytes: ByteArray) { + debug("Received ${bytes.size} byte(s)") + bytes.forEach(::readChar) + } + + override fun onDisconnected(thrown: Exception?) { + thrown?.let { e -> + errormsg("Serial error: $e") + } + debug("$device disconnected") + onDeviceDisconnect(false) + } + }).also { conn -> + connRef.set(conn) + conn.connect() + } } } override fun sendBytes(p: ByteArray) { - ioManager?.apply { - writeAsync(p) - } - } - - /** - * Called when [SerialInputOutputManager.run] aborts due to an error. - */ - override fun onRunError(e: java.lang.Exception) { - errormsg("Serial error: $e") - - onDeviceDisconnect(false) - } - - /** - * Called when new incoming data is available. - */ - override fun onNewData(data: ByteArray) { - data.forEach(::readChar) + connRef.get()?.sendBytes(p) } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt b/app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt index 717805ef6..8090a35cf 100644 --- a/app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt @@ -35,7 +35,7 @@ abstract class StreamInterface(protected val service: RadioInterfaceService) : * @param waitForStopped if true we should wait for the manager to finish - must be false if called from inside the manager callbacks * */ protected open fun onDeviceDisconnect(waitForStopped: Boolean) { - service.onDisconnect(isPermanent = true) // if USB device disconnects it is definitely permantently gone, not sleeping) + service.onDisconnect(isPermanent = true) // if USB device disconnects it is definitely permanently gone, not sleeping) } protected open fun connect() {