diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts index fad64e8dc..8a0578c8a 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -62,15 +62,21 @@ dependencies { implementation("com.google.flatbuffers:flatbuffers-java:22.10.26") implementation("com.illposed.osc:javaosc-core:0.9") - implementation("org.java-websocket:Java-WebSocket:1.+") implementation("com.melloware:jintellitype:1.+") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("com.mayakapps.kache:kache:2.1.1") - implementation("io.ktor:ktor-network:3.0.0") - implementation("io.ktor:ktor-utils:3.0.3") implementation("io.klogging:klogging:0.11.7") + val ktor_version = "3.4.1" + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") + implementation("io.ktor:ktor-utils:$ktor_version") + + api("com.github.loucass003:EspflashKotlin:v0.11.0") diff --git a/server/core/src/main/java/dev/slimevr/logger.kt b/server/core/src/main/java/dev/slimevr/logger.kt index 5f7380a64..c5539dad0 100644 --- a/server/core/src/main/java/dev/slimevr/logger.kt +++ b/server/core/src/main/java/dev/slimevr/logger.kt @@ -19,7 +19,6 @@ object AppLogger { fromMinLevel(Level.INFO) { toSink("stdout") } - } } } diff --git a/server/core/src/main/java/dev/slimevr/solarxr/server.kt b/server/core/src/main/java/dev/slimevr/solarxr/server.kt new file mode 100644 index 000000000..783f258bb --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/server.kt @@ -0,0 +1,59 @@ +package dev.slimevr.solarxr + +import io.ktor.server.application.* +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.routing.routing +import io.ktor.server.websocket.* +import io.ktor.websocket.Frame +import io.ktor.websocket.readBytes +import solarxr_protocol.MessageBundle +import java.nio.ByteBuffer + +const val SOLARXR_PORT = 21110; + +fun onSolarXRMessage(message: ByteBuffer) { + val messageBundle = MessageBundle.getRootAsMessageBundle(message) + + + for (index in 0.. { + val data = frame.readBytes() + onSolarXRMessage(frame.buffer) + println("Received Binary Packet: ${data.size} bytes") + } + else -> {} + } + + } + } + } + }.start(wait = true) +} + diff --git a/server/core/src/main/java/dev/slimevr/solarxr/solarxr.kt b/server/core/src/main/java/dev/slimevr/solarxr/solarxr.kt new file mode 100644 index 000000000..7f7d097cc --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/solarxr/solarxr.kt @@ -0,0 +1,6 @@ +package dev.slimevr.solarxr + + +data class SolarXRConnectionState( + val id: Int, +) diff --git a/server/core/src/main/java/dev/slimevr/tracker/device.kt b/server/core/src/main/java/dev/slimevr/tracker/device.kt index 3d146de33..865ed5f86 100644 --- a/server/core/src/main/java/dev/slimevr/tracker/device.kt +++ b/server/core/src/main/java/dev/slimevr/tracker/device.kt @@ -37,7 +37,7 @@ val DeviceStatsModule = DeviceModule( reducer = { s, a -> if (a is DeviceActions.Update) a.transform(s) else s }, observer = { it.state.onEach { state -> - AppLogger.device.info("Device state changed {State}", state) +// AppLogger.device.info("Device state changed", state) }.launchIn(it.scope) } ) diff --git a/server/core/src/main/java/dev/slimevr/tracker/tracker.kt b/server/core/src/main/java/dev/slimevr/tracker/tracker.kt index 7b96d20f1..7a5393305 100644 --- a/server/core/src/main/java/dev/slimevr/tracker/tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracker/tracker.kt @@ -55,7 +55,9 @@ data class Tracker( val TrackerInfosModule = TrackerModule( reducer = { s, a -> if (a is TrackerActions.Update) a.transform(s) else s }, observer = { - it.state.onEach { state -> AppLogger.tracker.info("Tracker state changed {State}", state) }.launchIn(it.scope) + it.state.onEach { state -> +// AppLogger.tracker.info("Tracker state changed {State}", state) + }.launchIn(it.scope) } ) diff --git a/server/core/src/main/java/dev/slimevr/tracker/udp/connection.kt b/server/core/src/main/java/dev/slimevr/tracker/udp/connection.kt index f2bd78803..8877f3304 100644 --- a/server/core/src/main/java/dev/slimevr/tracker/udp/connection.kt +++ b/server/core/src/main/java/dev/slimevr/tracker/udp/connection.kt @@ -55,7 +55,7 @@ data class UDPConnection( val context: UDPConnectionContext, val serverContext: VRServer, val packetEvents: PacketDispatcher, - val send: (Packet) -> Unit, + val send: (UDPPacket) -> Unit, val getDevice: () -> Device?, val getTracker: (sensorId: Int) -> Tracker?, ) @@ -88,19 +88,15 @@ val PacketModule = UDPConnectionModule( val now = System.currentTimeMillis() if (now - state.lastPacket > 5000 && packet.packetNumber == 0L) { - it.context.scope.launch { - it.context.dispatch( - UDPConnectionActions.LastPacket( - packetNum = 0, - time = now - ) + it.context.dispatch( + UDPConnectionActions.LastPacket( + packetNum = 0, + time = now ) - AppLogger.udp.info("Reconnecting") - } + ) + AppLogger.udp.info("Reconnecting") } else if (packet.packetNumber < state.lastPacketNum) { - it.context.scope.launch { - AppLogger.udp.warn("WARN: Received packet with wrong packet number") - } + AppLogger.udp.warn("WARN: Received packet with wrong packet number") return@onAny } else { it.context.scope.launch { @@ -139,14 +135,12 @@ val PingModule = UDPConnectionModule( } // listen for the pong - it.packetEvents.on { paket -> + it.packetEvents.on { packet -> val state = it.context.state.value val deviceId = state.deviceId ?: return@on - if (paket.data.pingId != state.lastPing.id + 1) { - it.context.scope.launch { - AppLogger.udp.warn("Ping ID does not match, ignoring ${paket.data.pingId} != ${state.lastPing.id + 1}") - } + if (packet.data.pingId != state.lastPing.id + 1) { + AppLogger.udp.warn("Ping ID does not match, ignoring ${packet.data.pingId} != ${state.lastPing.id + 1}") return@on } @@ -154,17 +148,15 @@ val PingModule = UDPConnectionModule( val device = it.serverContext.getDevice(deviceId) ?: return@on - it.context.scope.launch { - it.context.dispatch( - UDPConnectionActions.ReceivedPong( - id = paket.data.pingId, - duration = ping - ) + it.context.dispatch( + UDPConnectionActions.ReceivedPong( + id = packet.data.pingId, + duration = ping ) - device.context.dispatch(DeviceActions.Update { - copy(ping = ping) - }) - } + ) + device.context.dispatch(DeviceActions.Update { + copy(ping = ping) + }) } }, ) @@ -195,16 +187,14 @@ val HandshakeModule = UDPConnectionModule( serverContext = it.serverContext, ) - it.context.scope.launch { - it.serverContext.context.dispatch( - VRServerActions.NewDevice( - deviceId = deviceId, - context = newDevice - ) + it.serverContext.context.dispatch( + VRServerActions.NewDevice( + deviceId = deviceId, + context = newDevice ) - it.context.dispatch(UDPConnectionActions.Handshake(deviceId)) - it.send(Handshake()) - } + ) + it.context.dispatch(UDPConnectionActions.Handshake(deviceId)) + it.send(Handshake()) } else { it.send(Handshake()) } @@ -217,24 +207,20 @@ val DeviceStatsModule = UDPConnectionModule( it.packetEvents.on { event -> val device = it.getDevice() ?: return@on - device.context.scope.launch { - device.context.dispatch(DeviceActions.Update { - copy( - batteryLevel = event.data.level, - batteryVoltage = event.data.voltage - ) - }) - } + device.context.dispatch(DeviceActions.Update { + copy( + batteryLevel = event.data.level, + batteryVoltage = event.data.voltage + ) + }) } it.packetEvents.on { event -> val device = it.getDevice() ?: return@on - device.context.scope.launch { - device.context.dispatch(DeviceActions.Update { - copy(signalStrength = event.data.signal) - }) - } + device.context.dispatch(DeviceActions.Update { + copy(signalStrength = event.data.signal) + }) } } ) @@ -249,9 +235,9 @@ val SensorInfoModule = UDPConnectionModule( else -> s } }, - observer = { - it.packetEvents.on { event -> - val tracker = it.getTracker(event.data.sensorId) + observer = { observerContext -> + observerContext.packetEvents.on { event -> + val tracker = observerContext.getTracker(event.data.sensorId) val action = TrackerActions.Update { copy( @@ -261,41 +247,38 @@ val SensorInfoModule = UDPConnectionModule( } if (tracker != null) { - tracker.context.scope.launch { - tracker.context.dispatch(action) - } + tracker.context.dispatch(action) } else { - val device = it.getDevice() + + val device = observerContext.getDevice() ?: error("invalid state - a device should exist at this point") val deviceState = device.context.state.value - val trackerId = it.serverContext.nextHandle() + val trackerId = observerContext.serverContext.nextHandle() val newTracker = createTracker( id = trackerId, hardwareId = "${deviceState.address}:${event.data.sensorId}", sensorType = event.data.imuType, deviceId = deviceState.id, origin = DeviceOrigin.UDP, - serverContext = it.serverContext, - scope = it.serverContext.context.scope + serverContext = observerContext.serverContext, + scope = observerContext.serverContext.context.scope ) - it.serverContext.context.scope.launch { - it.serverContext.context.dispatch( - VRServerActions.NewTracker( - trackerId = trackerId, - context = newTracker + observerContext.serverContext.context.dispatch( + VRServerActions.NewTracker( + trackerId = trackerId, + context = newTracker + ) + ) + observerContext.context.dispatch( + UDPConnectionActions.AssignTracker( + trackerId = TrackerIdNum( + id = trackerId, + trackerNum = event.data.sensorId ) ) - it.context.dispatch( - UDPConnectionActions.AssignTracker( - trackerId = TrackerIdNum( - id = trackerId, - trackerNum = event.data.sensorId - ) - ) - ) - newTracker.context.dispatch(action) - } + ) + newTracker.context.dispatch(action) } } @@ -355,7 +338,7 @@ fun createUDPConnectionContext( context = context, serverContext = serverContext, dispatcher, - send = { packet: Packet -> + send = { packet: UDPPacket -> scope.launch { val bytePacket = buildPacket { writePacket(this, packet) diff --git a/server/core/src/main/java/dev/slimevr/tracker/udp/packets.kt b/server/core/src/main/java/dev/slimevr/tracker/udp/packets.kt index 599f75c21..262397311 100644 --- a/server/core/src/main/java/dev/slimevr/tracker/udp/packets.kt +++ b/server/core/src/main/java/dev/slimevr/tracker/udp/packets.kt @@ -5,13 +5,15 @@ import dev.slimevr.tracker.TrackerStatus import io.github.axisangles.ktmath.Quaternion import io.github.axisangles.ktmath.Vector3 import io.ktor.utils.io.core.remaining +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.io.Sink import kotlinx.io.Source import kotlinx.io.readByteArray import kotlinx.io.readFloat import kotlinx.io.readString import kotlinx.io.readUByte -import kotlinx.io.writeFloat import kotlinx.io.writeUByte import kotlin.reflect.KClass @@ -63,15 +65,15 @@ enum class PacketType(val id: Int) { } } -sealed interface Packet { +sealed interface UDPPacket { fun write(dst: Sink) {} } -sealed interface SensorSpecificPacket : Packet { +sealed interface SensorSpecificPacket : UDPPacket { val sensorId: Int } -data object Heartbeat : Packet +data object Heartbeat : UDPPacket data class Handshake( val boardType: Int = 0, @@ -80,7 +82,7 @@ data class Handshake( val protocolVersion: Int = 0, val firmware: String? = null, val macString: String? = null -) : Packet { +) : UDPPacket { override fun write(dst: Sink) { dst.writeByte(PacketType.HANDSHAKE.id.toByte()) dst.write("Hey OVR =D 5".toByteArray(Charsets.US_ASCII)) @@ -117,16 +119,16 @@ data class Accel(val acceleration: Vector3 = Vector3.NULL, override val sensorId } } -data class PingPong(val pingId: Int = 0) : Packet { +data class PingPong(val pingId: Int = 0) : UDPPacket { override fun write(dst: Sink) { dst.writeInt(pingId) } companion object { fun read(src: Source) = PingPong(src.readInt()) } } -data class Serial(val serial: String = "") : Packet { +data class Serial(val serial: String = "") : UDPPacket { companion object { fun read(src: Source) = Serial(src.readString(src.readInt().toLong())) } } -data class BatteryLevel(val voltage: Float = 0f, val level: Float = 0f) : Packet { +data class BatteryLevel(val voltage: Float = 0f, val level: Float = 0f) : UDPPacket { companion object { fun read(src: Source): BatteryLevel { val f = src.readSafeFloat() @@ -211,11 +213,11 @@ data class Temperature(override val sensorId: Int = 0, val temp: Float = 0f) : S } } -data class UserActionPacket(val type: Int = 0) : Packet { +data class UserActionPacket(val type: Int = 0) : UDPPacket { companion object { fun read(src: Source) = UserActionPacket(src.readU8()) } } -data class FeatureFlags(val firmwareFeatures: ByteArray = byteArrayOf()) : Packet { +data class FeatureFlags(val firmwareFeatures: ByteArray = byteArrayOf()) : UDPPacket { companion object { fun read(src: Source) = FeatureFlags(src.readByteArray(src.remaining.toInt())) } } @@ -261,7 +263,7 @@ data class PositionPacket(override val sensorId: Int = 0, val position: Vector3 } } -data class ProtocolChange(val targetProtocol: Int = 0, val targetVersion: Int = 0) : Packet { +data class ProtocolChange(val targetProtocol: Int = 0, val targetVersion: Int = 0) : UDPPacket { override fun write(dst: Sink) { dst.writeUByte(targetProtocol.toUByte()) dst.writeUByte(targetVersion.toUByte()) @@ -269,7 +271,7 @@ data class ProtocolChange(val targetProtocol: Int = 0, val targetVersion: Int = companion object { fun read(src: Source) = ProtocolChange(src.readU8(), src.readU8()) } } -fun readPacket(type: PacketType, src: Source): Packet = when (type) { +fun readPacket(type: PacketType, src: Source): UDPPacket = when (type) { PacketType.HEARTBEAT -> Heartbeat PacketType.HANDSHAKE -> Handshake.read(src) PacketType.ROTATION -> Rotation.read(src) @@ -295,7 +297,7 @@ fun readPacket(type: PacketType, src: Source): Packet = when (type) { PacketType.PROTOCOL_CHANGE -> ProtocolChange.read(src) } -fun writePacket(dst: Sink, packet: Packet) { +fun writePacket(dst: Sink, packet: UDPPacket) { val type = when (packet) { is Heartbeat -> PacketType.HEARTBEAT is Handshake -> PacketType.HANDSHAKE @@ -314,34 +316,38 @@ fun writePacket(dst: Sink, packet: Packet) { packet.write(dst) } -data class PacketEvent( +data class PacketEvent( val data: T, val packetNumber: Long, ) class PacketDispatcher { - val listeners = mutableMapOf, MutableList<(PacketEvent) -> Unit>>() - private val globalListeners = mutableListOf<(PacketEvent) -> Unit>() + val listeners = mutableMapOf, MutableList) -> Unit>>() + val globalListeners = mutableListOf) -> Unit>() + val mutex = Mutex() @Suppress("UNCHECKED_CAST") - inline fun on(crossinline callback: (PacketEvent) -> Unit) { - val list = listeners.getOrPut(T::class) { mutableListOf() } - synchronized(list) { - list.add { callback(it as PacketEvent) } + inline fun on(crossinline callback: suspend (PacketEvent) -> Unit) { + runBlocking { + mutex.withLock { + val list = listeners.getOrPut(T::class) { mutableListOf() } + list.add { callback(it as PacketEvent) } + } } } - fun onAny(callback: (PacketEvent) -> Unit) { - synchronized(globalListeners) { globalListeners.add(callback) } + fun onAny(callback: suspend (PacketEvent) -> Unit) { + runBlocking { + mutex.withLock { globalListeners.add(callback) } + } } - fun emit(event: PacketEvent) { - synchronized(globalListeners) { - globalListeners.forEach { it(event) } - } - val list = listeners[event.data::class] ?: return - synchronized(list) { - list.forEach { it(event) } + suspend fun emit(event: PacketEvent) { + val targets = mutex.withLock { + val specific = listeners[event.data::class]?.toList() ?: emptyList() + val global = globalListeners.toList() + global + specific } + targets.forEach { it(event) } } } diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt index e658a32a5..b5546429f 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt @@ -4,14 +4,20 @@ package dev.slimevr.desktop import dev.slimevr.VRServer import dev.slimevr.config.createConfig +import dev.slimevr.solarxr.createSolarXRWebsocketServer import dev.slimevr.tracker.udp.createUDPTrackerServer +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking fun main(args: Array) = runBlocking { val config = createConfig(this) val server = VRServer.create(this) - createUDPTrackerServer(server, config) - + launch { + createUDPTrackerServer(server, config) + } + launch { + createSolarXRWebsocketServer() + } Unit }