Skip to content

Commit

Permalink
Android serial (#888)
Browse files Browse the repository at this point in the history
  • Loading branch information
loucass003 authored Nov 18, 2023
1 parent eefe7ef commit 2dbc25b
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 1 deletion.
4 changes: 4 additions & 0 deletions server/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}

Expand All @@ -78,6 +79,9 @@ dependencies {
implementation("io.ktor:ktor-server-core:2.3.0")
implementation("io.ktor:ktor-server-netty:2.3.0")
implementation("io.ktor:ktor-server-caching-headers:2.3.0")

// Serial
implementation("com.github.mik3y:usb-serial-for-android:3.7.0")
}

/**
Expand Down
7 changes: 6 additions & 1 deletion server/android/src/main/java/dev/slimevr/android/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package dev.slimevr.android
import androidx.appcompat.app.AppCompatActivity
import dev.slimevr.Keybinding
import dev.slimevr.VRServer
import dev.slimevr.android.serial.AndroidSerialHandler
import io.eiren.util.logging.LogManager
import io.ktor.http.CacheControl
import io.ktor.http.CacheControl.Visibility
Expand Down Expand Up @@ -44,8 +45,12 @@ fun main(activity: AppCompatActivity) {
} catch (e1: java.lang.Exception) {
e1.printStackTrace()
}

try {
vrServer = VRServer(configPath = File(activity.filesDir, "vrconfig.yml").absolutePath)
vrServer = VRServer(
configPath = File(activity.filesDir, "vrconfig.yml").absolutePath,
serialHandlerProvider = { _ -> AndroidSerialHandler(activity) }
)
vrServer.start()
Keybinding(vrServer)
vrServer.join()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package dev.slimevr.android.serial

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.hardware.usb.UsbManager
import androidx.appcompat.app.AppCompatActivity
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
import dev.slimevr.serial.SerialHandler
import dev.slimevr.serial.SerialListener
import io.eiren.util.logging.LogManager
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.stream.Stream
import kotlin.concurrent.timerTask
import kotlin.streams.asSequence
import kotlin.streams.asStream
import dev.slimevr.serial.SerialPort as SlimeSerialPort

class SerialPortWrapper(val port: UsbSerialPort) : SlimeSerialPort() {
override val portLocation: String
get() = port.device.deviceName
override val descriptivePortName: String
get() = "${port.device.productName} (${port.device.deviceName})"

override val vendorId: Int
get() = port.device.vendorId

override val productId: Int
get() = port.device.productId
}

class AndroidSerialHandler(val activity: AppCompatActivity) :
SerialHandler(),
SerialInputOutputManager.Listener {

private var usbIoManager: SerialInputOutputManager? = null

private val listeners: MutableList<SerialListener> = CopyOnWriteArrayList()
private val getDevicesTimer = Timer("GetDevicesTimer")
private var watchingNewDevices = false
private var lastKnownPorts = setOf<SerialPortWrapper>()
private val manager = activity.getSystemService(Context.USB_SERVICE) as UsbManager
private var currentPort: SerialPortWrapper? = null
private var requestingPermission: String = ""

override val isConnected: Boolean
get() = currentPort?.port?.isOpen ?: false

override val knownPorts: Stream<SerialPortWrapper>
get() = getPorts()
.asSequence()
.map { SerialPortWrapper(it.ports[0]) }
.filter { isKnownBoard(it) }
.asStream()

init {
startWatchingNewDevices()
}

private fun getPorts(): List<UsbSerialDriver> {
return UsbSerialProber.getDefaultProber().findAllDrivers(manager)
}

private fun startWatchingNewDevices() {
if (watchingNewDevices) return
watchingNewDevices = true
getDevicesTimer.scheduleAtFixedRate(
timerTask {
try {
detectNewPorts()
} catch (t: Throwable) {
LogManager.severe(
"[SerialHandler] Error while watching for new devices, cancelling the \"getDevicesTimer\".",
t
)
getDevicesTimer.cancel()
}
},
0,
3000
)
}

private fun onNewDevice(port: SerialPortWrapper) {
listeners.forEach { it.onNewSerialDevice(port) }
}

private fun detectNewPorts() {
val differences = knownPorts.asSequence() - lastKnownPorts
lastKnownPorts = knownPorts.asSequence().toSet()
differences.forEach { onNewDevice(it) }
}

override fun addListener(channel: SerialListener) {
listeners.add(channel)
}

override fun removeListener(channel: SerialListener) {
listeners.removeIf { channel === it }
}

@Synchronized
override fun openSerial(portLocation: String?, auto: Boolean): Boolean {
LogManager.info("[SerialHandler] Trying to open: $portLocation, auto: $auto")
lastKnownPorts = knownPorts.asSequence().toSet()
val newPort = lastKnownPorts.find {
(!auto && it.portLocation == portLocation) || (auto && isKnownBoard(it))
}

if (newPort == null) {
LogManager.info(
"[SerialHandler] No serial ports found to connect to (${lastKnownPorts.size}) total ports"
)
return false
}

if (isConnected) {
val port = currentPort!!
if (newPort != port) {
LogManager.info(
"[SerialHandler] Closing current serial port " +
port.descriptivePortName
)
closeSerial()
} else {
LogManager.info("[SerialHandler] Reusing already open port")
listeners.forEach { it.onSerialConnected(port) }
return true
}
}

LogManager.info(
"[SerialHandler] Trying to connect to new serial port " +
newPort.descriptivePortName
)

if (!manager.hasPermission(newPort.port.device)) {
val flags = PendingIntent.FLAG_IMMUTABLE
val usbPermissionIntent = PendingIntent.getBroadcast(
activity,
0,
Intent(ACTION_USB_PERMISSION),
flags
)
if (requestingPermission != newPort.portLocation) {
println("Requesting permission for ${newPort.portLocation}")
manager.requestPermission(newPort.port.device, usbPermissionIntent)
requestingPermission = newPort.portLocation
}
LogManager.warning(
"[SerialHandler] Can't open serial port ${newPort.descriptivePortName}, invalid permissions"
)
return false
}

val connection = manager.openDevice(newPort.port.device)
if (connection == null) {
LogManager.warning(
"[SerialHandler] Can't open serial port ${newPort.descriptivePortName}, connection failed"

)
return false
}
newPort.port.open(connection)
newPort.port.setParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE)
usbIoManager = SerialInputOutputManager(newPort.port, this).apply {
start()
}
listeners.forEach { it.onSerialConnected(newPort) }
currentPort = newPort
LogManager.info("[SerialHandler] Serial port ${newPort.descriptivePortName} is open")
return true
}

@Synchronized
private fun writeSerial(serialText: String, print: Boolean = false) {
try {
usbIoManager?.writeAsync("${serialText}\n".toByteArray())
if (print) {
addLog("-> $serialText\n")
}
} catch (e: IOException) {
addLog("[!] Serial error: ${e.message}\n")
LogManager.warning("[SerialHandler] Serial port write error", e)
}
}

override fun rebootRequest() {
writeSerial("REBOOT")
}

override fun factoryResetRequest() {
writeSerial("FRST")
}

override fun infoRequest() {
writeSerial("GET INFO")
}

override fun closeSerial() {
try {
if (isConnected) {
currentPort?.port?.close()
}
listeners.forEach { it.onSerialDisconnected() }
LogManager.info(
"[SerialHandler] Port ${currentPort?.descriptivePortName} closed okay"
)
usbIoManager?.stop()
usbIoManager = null
currentPort = null
} catch (e: Exception) {
LogManager.warning(
"[SerialHandler] Error closing port ${currentPort?.descriptivePortName}",
e
)
}
}

@Synchronized
override fun setWifi(ssid: String, passwd: String) {
writeSerial("SET WIFI \"${ssid}\" \"${passwd}\"")
addLog("-> SET WIFI \"$ssid\" \"${passwd.replace(".".toRegex(), "*")}\"\n")
}

private fun addLog(str: String) {
LogManager.info("[Serial] $str")
listeners.forEach { it.onSerialLog(str) }
}

override fun onNewData(data: ByteArray?) {
if (data != null) {
val s = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(data)).toString()
addLog(s)
}
}

override fun onRunError(e: java.lang.Exception?) {}

companion object {
private val ACTION_USB_PERMISSION = "dev.slimevr.USB_PERMISSION"
}
}

0 comments on commit 2dbc25b

Please sign in to comment.