mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Android serial (#888)
This commit is contained in:
@@ -57,6 +57,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user