Merge pull request #411 from mcumings/serialC

Extract USB serial communication into USB repository
This commit is contained in:
Andre Kirchhoff 2022-04-09 08:27:03 -03:00 committed by GitHub
commit 1ca1642cb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 191 additions and 94 deletions

View file

@ -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()
}

View file

@ -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<UsbManager?>,
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<SerialInputOutputManager>()
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()
}
}

View file

@ -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?) {}
}

View file

@ -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()

View file

@ -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<SerialConnection?>()
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)
}
}

View file

@ -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() {