From e060bc7cc52eee33cc444c2b658b037d3cde79d0 Mon Sep 17 00:00:00 2001 From: loucass003 Date: Tue, 17 Mar 2026 21:22:56 +0100 Subject: [PATCH] Trying to get the basic shape of the state system out. Testing it with the udp part of the server to confirm the implementation choices --- flake.nix | 8 +- server/android/build.gradle.kts | 8 +- server/build.gradle.kts | 1 + server/core/build.gradle.kts | 8 +- .../src/main/java/dev/slimevr/VRServer.kt | 4 - .../main/java/dev/slimevr/config/config.kt | 67 +++++++ .../main/java/dev/slimevr/context/context.kt | 44 +++++ .../main/java/dev/slimevr/tracker/tracker.kt | 18 ++ .../dev/slimevr/tracker/udp/connection.kt | 138 +++++++++++++ .../java/dev/slimevr/tracker/udp/packets.kt | 187 ++++++++++++++++++ .../java/dev/slimevr/tracker/udp/server.kt | 86 ++++++++ .../src/main/java/dev/slimevr/vrserver.kt | 54 +++++ server/desktop/build.gradle.kts | 7 +- .../src/main/java/dev/slimevr/desktop/Main.kt | 12 +- 14 files changed, 623 insertions(+), 19 deletions(-) delete mode 100644 server/core/src/main/java/dev/slimevr/VRServer.kt create mode 100644 server/core/src/main/java/dev/slimevr/config/config.kt create mode 100644 server/core/src/main/java/dev/slimevr/context/context.kt create mode 100644 server/core/src/main/java/dev/slimevr/tracker/tracker.kt create mode 100644 server/core/src/main/java/dev/slimevr/tracker/udp/connection.kt create mode 100644 server/core/src/main/java/dev/slimevr/tracker/udp/packets.kt create mode 100644 server/core/src/main/java/dev/slimevr/tracker/udp/server.kt create mode 100644 server/core/src/main/java/dev/slimevr/vrserver.kt diff --git a/flake.nix b/flake.nix index 96e33f387..372fc5cad 100644 --- a/flake.nix +++ b/flake.nix @@ -12,8 +12,10 @@ perSystem = { pkgs, ... }: let + java = pkgs.jdk24; + runtimeLibs = pkgs: (with pkgs; [ - jdk22 + java alsa-lib at-spi2-atk at-spi2-core cairo cups dbus expat gdk-pixbuf glib gtk3 libdrm libgbm libglvnd libnotify @@ -33,8 +35,8 @@ name = "slimevr-env"; targetPkgs = runtimeLibs; profile = '' - export JAVA_HOME=${pkgs.jdk22} - export PATH="${pkgs.jdk22}/bin:$PATH" + export JAVA_HOME=${java} + export PATH="${java}/bin:$PATH" # Tell electron-builder to use system tools instead of downloading them export USE_SYSTEM_FPM=true diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index 694ff8392..39180108e 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -22,12 +22,12 @@ plugins { kotlin { jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(24)) } } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(24)) } } @@ -217,7 +217,7 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_22 - targetCompatibility = JavaVersion.VERSION_22 + sourceCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_24 } } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 6e0662e73..da4159f54 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -42,6 +42,7 @@ configure { ",dev.slimevr.tracking.trackers.*,dev.slimevr.desktop.platform.ProtobufMessages.*" + ",solarxr_protocol.rpc.*,kotlinx.coroutines.*,com.illposed.osc.*,android.app.*", "ij_kotlin_allow_trailing_comma" to true, + "ktlint_standard_filename" to "disabled", ) val ktlintVersion = "1.8.0" kotlinGradle { diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts index a98a9d9e2..26cacfa9d 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -16,17 +16,17 @@ plugins { kotlin { jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(24)) } } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(24)) } } tasks.withType { compilerOptions { - jvmTarget.set(JvmTarget.JVM_22) + jvmTarget.set(JvmTarget.JVM_24) freeCompilerArgs.set(listOf("-Xvalue-classes")) } } @@ -67,6 +67,8 @@ dependencies { 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") api("com.github.loucass003:EspflashKotlin:v0.11.0") diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt deleted file mode 100644 index b7fd794e3..000000000 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ /dev/null @@ -1,4 +0,0 @@ -package dev.slimevr - -class VRServer { -} diff --git a/server/core/src/main/java/dev/slimevr/config/config.kt b/server/core/src/main/java/dev/slimevr/config/config.kt new file mode 100644 index 000000000..2d7b8f191 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/config/config.kt @@ -0,0 +1,67 @@ +package dev.slimevr.config + +import dev.slimevr.context.Context +import dev.slimevr.context.BasicModule +import dev.slimevr.context.createContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.serialization.Serializable + +@Serializable +data class UserConfigState( + val userHeight: Float = 1.6f, +) + +@Serializable +data class SettingsConfigState( + val trackerPort: Int = 6969, +) + +@Serializable +data class GlobalConfigState( + val selectedUserProfile: String = "default", + val selectedSettingsProfile: String = "default", +) + +data class ConfigState( + val userConfig: UserConfigState = UserConfigState(), + val settingsConfig: SettingsConfigState = SettingsConfigState(), + val globalConfig: GlobalConfigState = GlobalConfigState(), +) + +sealed interface ConfigAction { + data class ChangeProfile(val settingsProfile: String? = null, val userProfile: String? = null) : ConfigAction +} + +typealias ConfigContext = Context +typealias ConfigModule = BasicModule + +val ConfigModuleTest = ConfigModule( + reducer = { s, a -> + when (a) { + is ConfigAction.ChangeProfile -> if (a.settingsProfile != null) { + s.copy(globalConfig = s.globalConfig.copy(selectedSettingsProfile = a.settingsProfile)) + } else if (a.userProfile != null) { + s.copy(globalConfig = s.globalConfig.copy(selectedUserProfile = a.userProfile)) + } else { + s + } + } + }, +) + +suspend fun createConfig(scope: CoroutineScope): ConfigContext { + + val modules = listOf(ConfigModuleTest) + + val context = createContext( + initialState = ConfigState(), + reducers = modules.map { it.reducer }, + scope = scope, + ) + + modules.map { it.observer }.forEach { it?.invoke(context) } + + context.dispatch(ConfigAction.ChangeProfile(settingsProfile = "Test")) + + return context +} diff --git a/server/core/src/main/java/dev/slimevr/context/context.kt b/server/core/src/main/java/dev/slimevr/context/context.kt new file mode 100644 index 000000000..d64b32200 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/context/context.kt @@ -0,0 +1,44 @@ +package dev.slimevr.context + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +data class BasicModule( + val reducer: (S, A) -> S, + val observer: ((Context) -> Unit)? = null, +) + +data class Context( + val state: StateFlow, + val dispatch: suspend (A) -> Unit, + val dispatchAll: suspend (List) -> Unit, + val scope: CoroutineScope, +) + +fun createContext( + initialState: S, + scope: CoroutineScope, + reducers: List<(S, A) -> S>, +): Context { + val mutableStateFlow = MutableStateFlow(initialState) + + val applyAction: (S, A) -> S = { currentState, action -> + reducers.fold(currentState) { s, reducer -> reducer(s, action) } + } + + val dispatch: suspend (A) -> Unit = { action -> + mutableStateFlow.update { applyAction(it, action) } + } + + val dispatchAll: suspend (List) -> Unit = { actions -> + mutableStateFlow.update { currentState -> + actions.fold(currentState) { s, action -> applyAction(s, action) } + } + } + val context = Context(mutableStateFlow.asStateFlow(), dispatch, dispatchAll, scope) + + return context +} diff --git a/server/core/src/main/java/dev/slimevr/tracker/tracker.kt b/server/core/src/main/java/dev/slimevr/tracker/tracker.kt new file mode 100644 index 000000000..ecb695607 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracker/tracker.kt @@ -0,0 +1,18 @@ +package dev.slimevr.tracker + +import dev.slimevr.context.Context +import dev.slimevr.context.BasicModule +import io.github.axisangles.ktmath.Quaternion + +data class TrackerState( + val id: Int, + val name: String, + val rawRotation: Quaternion, +) + +sealed interface TrackerActions { + data class UpdateRotation(val rotation: Quaternion) +} + +typealias TrackerContext = Context +typealias TrackerModule = BasicModule 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 new file mode 100644 index 000000000..ad472c83c --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracker/udp/connection.kt @@ -0,0 +1,138 @@ +package dev.slimevr.tracker.udp + +import dev.slimevr.context.Context +import dev.slimevr.context.createContext +import io.ktor.network.sockets.BoundDatagramSocket +import io.ktor.network.sockets.Datagram +import io.ktor.network.sockets.InetSocketAddress +import io.ktor.utils.io.core.buildPacket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +data class LastPing( + val id: Int, + val startTime: Long, + val duration: Long, +) + +data class UDPConnectionState( + val id: String, + val lastPacket: Long, + val lastPacketNum: Int, + val lastPing: LastPing, + val didHandshake: Boolean, + val address: String, + val port: Int, +) + +sealed interface UDPConnectionActions { + data class StartPing(val startTime: Long): UDPConnectionActions + data class ReceivedPong(val id: Int, val duration: Long): UDPConnectionActions +} + +typealias UDPConnectionContext = Context + +data class UDPConnection( + val context: UDPConnectionContext, + val packetEvents: PacketDispatcher, + val send: (Packet) -> Unit +) + +data class UDPConnectionModule( + val reducer: (UDPConnectionState, UDPConnectionActions) -> UDPConnectionState, + val observer: ((UDPConnection) -> Unit)? = null, +) + +val PingModule = UDPConnectionModule( + reducer = { s, a -> + when (a) { + is UDPConnectionActions.StartPing -> { + s.copy(lastPing = s.lastPing.copy(startTime = a.startTime)) + } + is UDPConnectionActions.ReceivedPong -> { + s.copy(lastPing = s.lastPing.copy(duration = a.duration, id = a.id)) + } + else -> s + } + }, + observer = { + // Send the ping every 1s + it.context.scope.launch { + while (isActive) { + val state = it.context.state.value + if (state.didHandshake) { + it.context.dispatch(UDPConnectionActions.StartPing(startTime = System.currentTimeMillis())) + it.send(PingPong(state.lastPacketNum + 1)) + } + delay(1000) + } + } + + // listen for the pong + it.packetEvents.on { packet -> + val state = it.context.state.value + if (packet.pingId != state.lastPing.id + 1) { + println("Ping ID does not match, ignoring") + return@on + } + + val ping = System.currentTimeMillis() - state.lastPing.startTime; + + it.context.scope.launch { + it.context.dispatch(UDPConnectionActions.ReceivedPong(id = packet.pingId, duration = ping)) + + // TODO update the device ping delay + } + } + } +) + + + +fun createUDPConnectionContext( + id: String, + socket: BoundDatagramSocket, + remoteAddress: InetSocketAddress, + scope: CoroutineScope +): UDPConnection { + + val modules = listOf(PingModule) + + val context = createContext( + initialState = UDPConnectionState( + id = id, + lastPacket = System.currentTimeMillis(), + lastPacketNum = 0, + lastPing = LastPing(id = 0, duration = 0, startTime = 0), + didHandshake = false, + address = remoteAddress.hostname, + port = remoteAddress.port, + ), + reducers = modules.map { it.reducer }, + scope = scope + ) + + val dispatcher = PacketDispatcher() + + val sendFunc = { packet: Packet -> + scope.launch { + val bytePacket = buildPacket { + writePacket(this, packet) + } + socket.send(Datagram(bytePacket, remoteAddress)) + } + Unit + } + + val conn = UDPConnection( + context, + dispatcher, + send = sendFunc + ) + + modules.map { it.observer }.forEach { it?.invoke(conn) } + + return conn +} 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 new file mode 100644 index 000000000..ed44a82e4 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracker/udp/packets.kt @@ -0,0 +1,187 @@ +package dev.slimevr.tracker.udp + +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import io.ktor.utils.io.core.remaining +import kotlinx.io.* +import kotlin.reflect.KClass + +// ── Enums ─────────────────────────────────────────────────────────────────── + +enum class PacketType(val id: Int) { + HEARTBEAT(0), ROTATION(1), HANDSHAKE(3), ACCEL(4), PING_PONG(10), + SERIAL(11), BATTERY_LEVEL(12), TAP(13), ERROR(14), SENSOR_INFO(15), + ROTATION_2(16), ROTATION_DATA(17), MAGNETOMETER_ACCURACY(18), + SIGNAL_STRENGTH(19), TEMPERATURE(20), USER_ACTION(21), PROTOCOL_CHANGE(200); + + companion object { + private val map = entries.associateBy { it.id } + fun fromId(id: Int) = map[id] + } +} + +enum class UserAction(val id: Int) { + RESET_FULL(2), RESET_YAW(3), RESET_MOUNTING(4), PAUSE_TRACKING(5); + companion object { + private val map = entries.associateBy { it.id } + fun fromId(id: Int) = map[id] + } +} + +sealed interface Packet +sealed interface SensorSpecificPacket : Packet { val sensorId: Int } +sealed interface RotationPacket : SensorSpecificPacket { val rotation: Quaternion } + +data object Heartbeat : Packet +data class Handshake(val board: Int = 0, val imu: Int = 0, val mcu: Int = 0, val pVer: Int = 0, val firmware: String? = null, val mac: String? = null) : Packet +data class Rotation(override val sensorId: Int = 0, override val rotation: Quaternion = Quaternion.IDENTITY) : RotationPacket +data class Accel(val acceleration: Vector3 = Vector3.NULL, override val sensorId: Int = 0) : SensorSpecificPacket +data class PingPong(val pingId: Int = 0) : Packet +data class Serial(val serial: String = "") : Packet +data class BatteryLevel(val voltage: Float = 0f, val level: Float = 0f) : Packet +data class Tap(override val sensorId: Int = 0, val tap: Int = 0) : SensorSpecificPacket +data class Error(override val sensorId: Int = 0, val errorNumber: Int = 0) : SensorSpecificPacket +data class SensorInfo(override val sensorId: Int = 0, val status: Int = 0, val type: Int = 0) : SensorSpecificPacket +data class Rotation2(override val sensorId: Int = 1, override val rotation: Quaternion = Quaternion.IDENTITY) : RotationPacket +data class RotationData(override val sensorId: Int = 0, val dataType: Int = 0, val rotation: Quaternion = Quaternion.IDENTITY, val calibration: Int = 0) : SensorSpecificPacket +data class MagnetometerAccuracy(override val sensorId: Int = 0, val accuracy: Float = 0f) : SensorSpecificPacket +data class SignalStrength(override val sensorId: Int = 0, val signal: Int = 0) : SensorSpecificPacket +data class Temperature(override val sensorId: Int = 0, val temp: Float = 0f) : SensorSpecificPacket +data class UserActionPacket(val action: UserAction? = null) : Packet +data class ProtocolChange(val targetProtocol: Int = 0, val targetVersion: Int = 0) : Packet + +private fun Source.readU8() = readByte().toInt() and 0xFF +private fun Sink.writeU8(v: Int) = writeByte(v.toByte()) + +private fun Source.readSafeFloat() = readFloat().let { if (it.isNaN()) 0f else it } +private fun Source.readSafeQuat() = Quaternion(readFloat(), readFloat(), readFloat(), readFloat()).let { + if (it.x.isNaN() || (it.x == 0f && it.y == 0f && it.z == 0f && it.w == 0f)) Quaternion.IDENTITY else it +} + +private fun Sink.writeQuat(q: Quaternion) { + writeFloat(q.x); writeFloat(q.y); writeFloat(q.z); writeFloat(q.w) +} + +private fun Source.readStr(len: Int) = buildString { + repeat(len) { readByte().toInt().takeIf { it != 0 }?.let { append(it.toChar()) } } +} + +object PacketCodec { + fun read(type: PacketType, src: Source): Packet = when (type) { + PacketType.HEARTBEAT -> Heartbeat + PacketType.HANDSHAKE -> with(src) { + if (exhausted()) return Handshake() + val b = if (remaining >= 4) readInt() else 0 + val i = if (remaining >= 4) readInt() else 0 + val m = if (remaining >= 4) readInt() else 0 + if (remaining >= 12) skip(12) + val p = if (remaining >= 4) readInt() else 0 + val f = if (remaining >= 1) readStr(readByte().toInt()) else null + val mac = if (remaining >= 6) readByteArray(6).joinToString(":") { "%02X".format(it) }.takeIf { it != "00:00:00:00:00:00" } else null + Handshake(b, i, m, p, f, mac) + } + PacketType.ROTATION -> Rotation(rotation = src.readSafeQuat()) + PacketType.ACCEL -> Accel(Vector3(src.readSafeFloat(), src.readSafeFloat(), src.readSafeFloat()), if (!src.exhausted()) src.readU8() else 0) + PacketType.PING_PONG -> PingPong(src.readInt()) + PacketType.SERIAL -> Serial(src.readStr(src.readInt())) + PacketType.BATTERY_LEVEL -> src.readSafeFloat().let { f -> + if (src.remaining >= 4) BatteryLevel(f, src.readSafeFloat()) else BatteryLevel(0f, f) + } + PacketType.TAP -> Tap(src.readU8(), src.readU8()) + PacketType.ERROR -> Error(src.readU8(), src.readU8()) + PacketType.SENSOR_INFO -> SensorInfo(src.readU8(), src.readU8(), if (!src.exhausted()) src.readU8() else 0) + PacketType.ROTATION_2 -> Rotation2(rotation = src.readSafeQuat()) + PacketType.ROTATION_DATA -> RotationData(src.readU8(), src.readU8(), src.readSafeQuat(), src.readU8()) + PacketType.MAGNETOMETER_ACCURACY -> MagnetometerAccuracy(src.readU8(), src.readSafeFloat()) + PacketType.SIGNAL_STRENGTH -> SignalStrength(src.readU8(), src.readByte().toInt()) + PacketType.TEMPERATURE -> Temperature(src.readU8(), src.readSafeFloat()) + PacketType.USER_ACTION -> UserActionPacket(UserAction.fromId(src.readU8())) + PacketType.PROTOCOL_CHANGE -> ProtocolChange(src.readU8(), src.readU8()) + } + + fun write(dst: Sink, packet: Packet) = when (packet) { + is Heartbeat -> {} + is Handshake -> { + dst.writeU8(PacketType.HANDSHAKE.id) + dst.write("Hey OVR =D 5".toByteArray(Charsets.US_ASCII)) + } + is Rotation -> dst.writeQuat(packet.rotation) + is Accel -> { dst.writeFloat(packet.acceleration.x); dst.writeFloat(packet.acceleration.y); dst.writeFloat(packet.acceleration.z) } + is PingPong -> dst.writeInt(packet.pingId) + is Serial -> { dst.writeInt(packet.serial.length); packet.serial.forEach { dst.writeU8(it.code) } } + is BatteryLevel -> { dst.writeFloat(packet.voltage); dst.writeFloat(packet.level) } + is Tap -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.tap) } + is Error -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.errorNumber) } + is SensorInfo -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.status); dst.writeU8(packet.type) } + is Rotation2 -> dst.writeQuat(packet.rotation) + is RotationData -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.dataType); dst.writeQuat(packet.rotation); dst.writeU8(packet.calibration) } + is MagnetometerAccuracy -> { dst.writeU8(packet.sensorId); dst.writeFloat(packet.accuracy) } + is SignalStrength -> { dst.writeU8(packet.sensorId); dst.writeByte(packet.signal.toByte()) } + is Temperature -> { dst.writeU8(packet.sensorId); dst.writeFloat(packet.temp) } + is UserActionPacket -> dst.writeU8(packet.action?.id ?: 0) + is ProtocolChange -> { dst.writeU8(packet.targetProtocol); dst.writeU8(packet.targetVersion) } + } +} + +// ── Entry Points ──────────────────────────────────────────────────────────── + +fun readPacket(src: Source): Packet? { + if (src.exhausted()) return null + val type = PacketType.fromId(src.readInt()) ?: return null + if (type != PacketType.HANDSHAKE) src.skip(8) // Skip sequence number + return PacketCodec.read(type, src) +} + +fun writePacket(dst: Sink, packet: Packet) { + val type = when(packet) { + is Heartbeat -> PacketType.HEARTBEAT + is Handshake -> PacketType.HANDSHAKE + is Rotation -> PacketType.ROTATION + is Accel -> PacketType.ACCEL + is PingPong -> PacketType.PING_PONG + is Serial -> PacketType.SERIAL + is BatteryLevel -> PacketType.BATTERY_LEVEL + is Tap -> PacketType.TAP + is Error -> PacketType.ERROR + is SensorInfo -> PacketType.SENSOR_INFO + is Rotation2 -> PacketType.ROTATION_2 + is RotationData -> PacketType.ROTATION_DATA + is MagnetometerAccuracy -> PacketType.MAGNETOMETER_ACCURACY + is SignalStrength -> PacketType.SIGNAL_STRENGTH + is Temperature -> PacketType.TEMPERATURE + is UserActionPacket -> PacketType.USER_ACTION + is ProtocolChange -> PacketType.PROTOCOL_CHANGE + } + + if (type != PacketType.HANDSHAKE) { + dst.writeInt(type.id) + dst.writeLong(type.id.toLong()) // Sequence number placeholder + } + PacketCodec.write(dst, packet) +} + + +class PacketDispatcher { + val listeners = mutableMapOf, MutableList<(Packet) -> Unit>>() + + /** + * Listen for a specific packet type. + * Usage: dispatcher.on { packet -> println(packet.rotation) } + */ + inline fun on(crossinline callback: (T) -> Unit) { + val list = listeners.getOrPut(T::class) { mutableListOf() } + synchronized(list) { + list.add { callback(it as T) } + } + } + + /** + * Broadcasts a packet to all registered listeners for its type. + */ + fun emit(packet: Packet) { + val list = listeners[packet::class] ?: return + synchronized(list) { + list.forEach { it(packet) } + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/tracker/udp/server.kt b/server/core/src/main/java/dev/slimevr/tracker/udp/server.kt new file mode 100644 index 000000000..c4aea1580 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracker/udp/server.kt @@ -0,0 +1,86 @@ +package dev.slimevr.tracker.udp + +import dev.slimevr.VRServerContext +import dev.slimevr.config.ConfigContext +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.BoundDatagramSocket +import io.ktor.network.sockets.Datagram +import io.ktor.network.sockets.InetSocketAddress +import io.ktor.network.sockets.aSocket +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope + +data class UDPTrackerServerState( + val port: Int, + val connections: MutableMap +) + +suspend fun processPacket( + socket: BoundDatagramSocket, + datagram: Datagram, + workerId: Int +) { + + +} + +const val PACKET_WORKERS = 4 + +suspend fun createUDPTrackerServer( + serverContext: VRServerContext, + configContext: ConfigContext +): UDPTrackerServerState { + val state = UDPTrackerServerState( + port = configContext.state.value.settingsConfig.trackerPort, + connections = mutableMapOf() + ) + + val selectorManager = SelectorManager(Dispatchers.IO) + val serverSocket = + aSocket(selectorManager).udp().bind(InetSocketAddress("0.0.0.0", state.port)) + val packetChannel = Channel(capacity = Channel.BUFFERED) + + supervisorScope { + launch(Dispatchers.IO) { + while (isActive) { + val datagram = serverSocket.receive() + packetChannel.send(datagram) + } + } + + repeat(PACKET_WORKERS) { workerId -> + launch(Dispatchers.Default) { + for (datagram in packetChannel) { + val packet = readPacket(datagram.packet) + if (packet == null) { + println("null packet") + continue + } + + val address = datagram.address as InetSocketAddress + val connContext = state.connections[address.hostname] + + if (connContext !== null) + connContext.packetEvents.emit(packet = packet) + else { + val newContext = createUDPConnectionContext( + id = address.hostname, + remoteAddress = address, + socket = serverSocket, + scope = this + ) + + state.connections[address.hostname] = newContext + newContext.packetEvents.emit(packet = packet) + } + + } + } + } + } + + return state +} diff --git a/server/core/src/main/java/dev/slimevr/vrserver.kt b/server/core/src/main/java/dev/slimevr/vrserver.kt new file mode 100644 index 000000000..d2257ae77 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/vrserver.kt @@ -0,0 +1,54 @@ +package dev.slimevr + +import dev.slimevr.context.Context +import dev.slimevr.context.BasicModule +import dev.slimevr.context.createContext +import dev.slimevr.tracker.TrackerContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +data class VRServerState( + val trackers: Map, +) + +sealed interface VRServerActions { + data class NewTracker(val trackerId: Int, val context: TrackerContext) : VRServerActions +} + +typealias VRServerContext = Context +typealias VRServerModule = BasicModule + +val TestModule = VRServerModule( + reducer = { s, a -> + when (a) { + is VRServerActions.NewTracker -> s.copy( + trackers = s.trackers + (a.trackerId to a.context), + ) + } + }, + observer = { context -> + context.state.distinctUntilChangedBy { state -> state.trackers.size }.onEach { + println("tracker list size changed") + }.launchIn(context.scope) + }, +) + +fun createVRServer(scope: CoroutineScope): VRServerContext { + val server = VRServerState( + trackers = mapOf(), + ) + + val modules = listOf(TestModule) + + val context = createContext( + initialState = server, + reducers = modules.map { it.reducer }, + scope = scope, + ) + + modules.map { it.observer }.forEach { it?.invoke(context) } + + return context +} diff --git a/server/desktop/build.gradle.kts b/server/desktop/build.gradle.kts index c71c9dd3e..38eea50a4 100644 --- a/server/desktop/build.gradle.kts +++ b/server/desktop/build.gradle.kts @@ -18,17 +18,17 @@ plugins { kotlin { jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(24)) } } java { toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) + languageVersion.set(JavaLanguageVersion.of(24)) } } tasks.withType { compilerOptions { - jvmTarget.set(JvmTarget.JVM_22) + jvmTarget.set(JvmTarget.JVM_24) freeCompilerArgs.set(listOf("-Xvalue-classes")) } } @@ -60,6 +60,7 @@ dependencies { implementation("net.java.dev.jna:jna:5.+") implementation("net.java.dev.jna:jna-platform:5.+") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("com.fazecast:jSerialComm:2.11.3") { exclude(group = "com.fazecast", module = "android") } 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 c4c3412fa..6f1024e0e 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt @@ -2,6 +2,14 @@ package dev.slimevr.desktop -fun main(args: Array) { - println("Hello world!") +import dev.slimevr.config.createConfig +import dev.slimevr.createVRServer +import dev.slimevr.tracker.udp.createUDPTrackerServer +import kotlinx.coroutines.runBlocking + +fun main(args: Array) = runBlocking { + val config = createConfig(this) + val server = createVRServer(this) + + val udpServer = createUDPTrackerServer(server, config) }