Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android serial #888

Merged
merged 5 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,256 @@
package dev.slimevr.android.serial

import android.app.PendingIntent
import android.content.BroadcastReceiver
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.io.OutputStreamWriter
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"
}


}