Meshtastic-Android/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt

288 lines
10 KiB
Kotlin
Raw Normal View History

2020-06-05 11:53:50 -07:00
package com.geeksville.mesh.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import com.geeksville.android.Logging
import com.geeksville.mesh.android.usbManager
import com.geeksville.util.exceptionReporter
import com.geeksville.util.ignoreException
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.hoho.android.usbserial.util.SerialInputOutputManager
2020-06-05 11:53:50 -07:00
class SerialInterface(private val service: RadioInterfaceService, val address: String) : Logging,
IRadioInterface, SerialInputOutputManager.Listener {
2020-06-08 14:04:56 -07:00
companion object : Logging {
private const val START1 = 0x94.toByte()
private const val START2 = 0xc3.toByte()
private const val MAX_TO_FROM_RADIO_SIZE = 512
2020-06-05 11:53:50 -07:00
/**
* according to https://stackoverflow.com/questions/12388914/usb-device-access-pop-up-suppression/15151075#15151075
* we should never ask for USB permissions ourselves, instead we should rely on the external dialog printed by the system. If
* we do that the system will remember we have accesss
*/
val assumePermission = true
2020-06-11 11:21:11 -07:00
fun toInterfaceName(deviceName: String) = "s$deviceName"
2020-06-08 14:04:56 -07:00
fun findDrivers(context: Context): List<UsbSerialDriver> {
val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(context.usbManager)
2020-06-08 14:04:56 -07:00
val devices = drivers.map { it.device }
devices.forEach { d ->
debug("Found serial port ${d.deviceName}")
2020-06-08 14:04:56 -07:00
}
return drivers
}
fun addressValid(context: Context, rest: String): Boolean {
findSerial(context, rest)?.let { d ->
return assumePermission || context.usbManager.hasPermission(d.device)
}
return false
}
fun findSerial(context: Context, rest: String): UsbSerialDriver? {
val drivers = findDrivers(context)
return if (drivers.isEmpty())
null
else // Open a connection to the first available driver.
drivers[0] // FIXME, instead we should find by name
}
2020-06-05 11:53:50 -07:00
}
private var uart: UsbSerialDriver? = null
private var ioManager: SerialInputOutputManager? = null
2020-06-08 14:04:56 -07:00
var usbReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
debug("A USB device was detached")
val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
if (uart?.device == device)
onDeviceDisconnect(true)
}
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
debug("attaching USB")
val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
if (assumePermission || context.usbManager.hasPermission(device)) {
// reinit the port from scratch and reopen
onDeviceDisconnect(true)
connect()
} else {
warn("We don't have permissions for this USB device")
}
}
}
}
2020-06-11 11:21:11 -07:00
private val debugLineBuf = kotlin.text.StringBuilder()
/** The index of the next byte we are hoping to receive */
private var ptr = 0
/** The two halves of our length */
private var msb = 0
private var lsb = 0
private var packetLen = 0
init {
val filter = IntentFilter()
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
service.registerReceiver(usbReceiver, filter)
connect()
}
override fun close() {
debug("Closing serial port for good")
service.unregisterReceiver(usbReceiver)
onDeviceDisconnect(true)
}
2020-06-08 14:04:56 -07:00
/** 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
* */
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
}
}
service.onDisconnect(isPermanent = true) // if USB device disconnects it is definitely permantently gone, not sleeping)
}
private fun connect() {
val manager = service.getSystemService(Context.USB_SERVICE) as UsbManager
val device = findSerial(service, 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)
2020-06-11 11:21:11 -07:00
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
2020-06-11 11:21:11 -07:00
// Before telling mesh service, send a few START1s to wake a sleeping device
val wakeBytes = byteArrayOf(START1, START1, START1, START1)
io.writeAsync(wakeBytes)
// Now tell clients they can (finally use the api)
2020-06-14 16:43:36 -07:00
service.onConnect()
}
} else {
errormsg("Can't find device")
}
}
2020-06-05 11:53:50 -07:00
override fun handleSendToRadio(p: ByteArray) {
2020-06-08 14:04:56 -07:00
// This method is called from a continuation and it might show up late, so check for uart being null
val header = ByteArray(4)
header[0] = START1
header[1] = START2
header[2] = (p.size shr 8).toByte()
header[3] = (p.size and 0xff).toByte()
ioManager?.apply {
writeAsync(header)
writeAsync(p)
2020-06-05 11:53:50 -07:00
}
}
2020-06-11 11:21:11 -07:00
/** Print device serial debug output somewhere */
2020-06-11 11:21:11 -07:00
private fun debugOut(b: Byte) {
when (val c = b.toChar()) {
'\r' -> {
} // ignore
'\n' -> {
debug("DeviceLog: $debugLineBuf")
debugLineBuf.clear()
}
else ->
debugLineBuf.append(c)
}
}
2020-06-08 19:15:18 -07:00
private val rxPacket = ByteArray(MAX_TO_FROM_RADIO_SIZE)
private fun readChar(c: Byte) {
// Assume we will be advancing our pointer
var nextPtr = ptr + 1
fun lostSync() {
errormsg("Lost protocol sync")
nextPtr = 0
}
/// Deliver our current packet and restart our reader
fun deliverPacket() {
val buf = rxPacket.copyOf(packetLen)
service.handleFromRadio(buf)
nextPtr = 0 // Start parsing the next packet
}
2020-06-08 19:15:18 -07:00
when (ptr) {
0 -> // looking for START1
if (c != START1) {
debugOut(c)
nextPtr = 0 // Restart from scratch
}
1 -> // Looking for START2
if (c != START2)
lostSync() // Restart from scratch
2020-06-08 19:15:18 -07:00
2 -> // Looking for MSB of our 16 bit length
msb = c.toInt() and 0xff
3 -> { // Looking for LSB of our 16 bit length
2020-06-08 19:15:18 -07:00
lsb = c.toInt() and 0xff
// We've read our header, do one big read for the packet itself
2020-06-08 19:15:18 -07:00
packetLen = (msb shl 8) or lsb
if (packetLen > MAX_TO_FROM_RADIO_SIZE)
lostSync() // If packet len is too long, the bytes must have been corrupted, start looking for START1 again
else if (packetLen == 0)
deliverPacket() // zero length packets are valid and should be delivered immediately (because there won't be a next byte of payload)
2020-06-08 19:15:18 -07:00
}
else -> {
// We are looking at the packet bytes now
rxPacket[ptr - 4] = c
2020-06-08 19:15:18 -07:00
// Note: we have to check if ptr +1 is equal to packet length (for example, for a 1 byte packetlen, this code will be run with ptr of4
if (ptr - 4 + 1 == packetLen) {
deliverPacket()
2020-06-05 11:53:50 -07:00
}
}
}
2020-06-08 19:15:18 -07:00
ptr = nextPtr
}
2020-06-05 11:53:50 -07:00
/**
* 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) {
2020-06-08 19:15:18 -07:00
data.forEach(::readChar)
}
2020-06-05 11:53:50 -07:00
}