mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Merge pull request #411 from mcumings/serialC
Extract USB serial communication into USB repository
This commit is contained in:
commit
1ca1642cb9
6 changed files with 191 additions and 94 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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?) {}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue