basic handshake

This commit is contained in:
loucass003
2026-03-18 16:38:20 +01:00
parent e060bc7cc5
commit 01b7b8dbba
8 changed files with 411 additions and 106 deletions

View File

@@ -1,7 +1,7 @@
package dev.slimevr.config
import dev.slimevr.context.Context
import dev.slimevr.context.BasicModule
import dev.slimevr.context.Context
import dev.slimevr.context.createContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.Serializable
@@ -50,7 +50,6 @@ val ConfigModuleTest = ConfigModule(
)
suspend fun createConfig(scope: CoroutineScope): ConfigContext {
val modules = listOf(ConfigModuleTest)
val context = createContext(

View File

@@ -0,0 +1,86 @@
package dev.slimevr.tracker
import dev.slimevr.VRServer
import dev.slimevr.context.BasicModule
import dev.slimevr.context.Context
import dev.slimevr.context.createContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
enum class DeviceOrigin {
DRIVER,
FEEDER,
UDP,
HID,
}
data class DeviceState(
val id: Int,
val name: String,
val address: String,
val batteryLevel: Int,
val batteryVoltage: Int,
val ping: Long?,
val signalStrength: Int?,
val origin: DeviceOrigin,
)
sealed interface DeviceActions {
data class SetBattery(val level: Int, val voltage: Int) : DeviceActions
data class SetPing(val ping: Long) : DeviceActions
data class SetSignalStrength(val signalStrength: Int) : DeviceActions
}
typealias DeviceContext = Context<DeviceState, DeviceActions>
typealias DeviceModule = BasicModule<DeviceState, DeviceActions>
data class Device(
val context: DeviceContext,
)
val PingModule = DeviceModule(
reducer = { s, a ->
when (a) {
is DeviceActions.SetPing -> s.copy(ping = a.ping)
else -> s
}
},
observer = {
it.state
.distinctUntilChangedBy { device -> device.ping }
.filter { device -> device.ping != null }
.onEach { device ->
println("[${device.name}] ping change to ${device.ping}")
}.launchIn(it.scope)
},
)
fun createDevice(scope: CoroutineScope, id: Int, address: String, origin: DeviceOrigin, serverContext: VRServer): Device {
val deviceState = DeviceState(
id = id,
name = "Device $id",
batteryLevel = 0,
batteryVoltage = 0,
origin = origin,
address = address,
ping = null,
signalStrength = null,
)
val modules = listOf(PingModule)
val context = createContext(
initialState = deviceState,
reducers = modules.map { it.reducer },
scope = scope,
)
modules.map { it.observer }.forEach { it?.invoke(context) }
return Device(
context = context,
)
}

View File

@@ -1,7 +1,7 @@
package dev.slimevr.tracker
import dev.slimevr.context.Context
import dev.slimevr.context.BasicModule
import dev.slimevr.context.Context
import io.github.axisangles.ktmath.Quaternion
data class TrackerState(

View File

@@ -1,7 +1,12 @@
package dev.slimevr.tracker.udp
import dev.slimevr.VRServer
import dev.slimevr.VRServerActions
import dev.slimevr.context.Context
import dev.slimevr.context.createContext
import dev.slimevr.tracker.DeviceActions
import dev.slimevr.tracker.DeviceOrigin
import dev.slimevr.tracker.createDevice
import io.ktor.network.sockets.BoundDatagramSocket
import io.ktor.network.sockets.Datagram
import io.ktor.network.sockets.InetSocketAddress
@@ -20,24 +25,28 @@ data class LastPing(
data class UDPConnectionState(
val id: String,
val lastPacket: Long,
val lastPacketNum: Int,
val lastPacketNum: Long,
val lastPing: LastPing,
val didHandshake: Boolean,
val address: String,
val port: Int,
val deviceId: Int?,
)
sealed interface UDPConnectionActions {
data class StartPing(val startTime: Long): UDPConnectionActions
data class ReceivedPong(val id: Int, val duration: Long): UDPConnectionActions
data class StartPing(val startTime: Long) : UDPConnectionActions
data class ReceivedPong(val id: Int, val duration: Long) : UDPConnectionActions
data class Handshake(val deviceId: Int) : UDPConnectionActions
data class LastPacket(val packetNum: Long? = null, val time: Long) : UDPConnectionActions
}
typealias UDPConnectionContext = Context<UDPConnectionState, UDPConnectionActions>
data class UDPConnection(
val context: UDPConnectionContext,
val serverContext: VRServer,
val packetEvents: PacketDispatcher,
val send: (Packet) -> Unit
val send: (Packet) -> Unit,
)
data class UDPConnectionModule(
@@ -45,15 +54,55 @@ data class UDPConnectionModule(
val observer: ((UDPConnection) -> Unit)? = null,
)
val PacketModule = UDPConnectionModule(
reducer = { s, a ->
when (a) {
is UDPConnectionActions.LastPacket -> {
var newState = s.copy(lastPacket = a.time)
if (a.packetNum != null) {
newState = newState.copy(lastPacketNum = a.packetNum)
}
newState
}
else -> s
}
},
observer = {
it.packetEvents.onAny { packet ->
val state = it.context.state.value
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))
println("Reconnecting")
}
} else if (packet.packetNumber < state.lastPacketNum) {
println("WARN: Received packet with wrong packet number")
return@onAny
} else {
it.context.scope.launch {
it.context.dispatch(UDPConnectionActions.LastPacket(time = now))
}
}
}
},
)
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
}
},
@@ -64,41 +113,76 @@ val PingModule = UDPConnectionModule(
val state = it.context.state.value
if (state.didHandshake) {
it.context.dispatch(UDPConnectionActions.StartPing(startTime = System.currentTimeMillis()))
it.send(PingPong(state.lastPacketNum + 1))
it.send(PingPong(state.lastPing.id + 1))
}
delay(1000)
}
}
// listen for the pong
it.packetEvents.on<PingPong> { packet ->
it.packetEvents.on<PingPong> { paket ->
val state = it.context.state.value
if (packet.pingId != state.lastPing.id + 1) {
val deviceId = state.deviceId ?: return@on
if (paket.data.pingId != state.lastPing.id + 1) {
println("Ping ID does not match, ignoring")
return@on
}
val ping = System.currentTimeMillis() - state.lastPing.startTime;
val ping = System.currentTimeMillis() - state.lastPing.startTime
val device = it.serverContext.getDeviceContext(deviceId) ?: return@on
it.context.scope.launch {
it.context.dispatch(UDPConnectionActions.ReceivedPong(id = packet.pingId, duration = ping))
// TODO update the device ping delay
it.context.dispatch(UDPConnectionActions.ReceivedPong(id = paket.data.pingId, duration = ping))
device.context.dispatch(DeviceActions.SetPing(ping))
}
}
}
},
)
val HandshakeModule = UDPConnectionModule(
reducer = { s, a ->
when (a) {
is UDPConnectionActions.Handshake -> s.copy(didHandshake = true, deviceId = a.deviceId)
else -> s
}
},
observer = {
it.packetEvents.on<Handshake> { packet ->
val state = it.context.state.value
if (state.deviceId == null) {
val deviceId = it.serverContext.nextHandle()
val newDevice = createDevice(
id = deviceId,
scope = it.serverContext.context.scope,
address = packet.data.mac ?: error("no mac address?"),
origin = DeviceOrigin.UDP,
serverContext = it.serverContext,
)
it.context.scope.launch {
it.serverContext.context.dispatch(VRServerActions.NewDevice(deviceId = deviceId, context = newDevice))
it.context.dispatch(UDPConnectionActions.Handshake(deviceId))
it.send(HandshakeResponse())
}
} else {
it.send(HandshakeResponse())
}
}
},
)
fun createUDPConnectionContext(
id: String,
socket: BoundDatagramSocket,
remoteAddress: InetSocketAddress,
scope: CoroutineScope
serverContext: VRServer,
scope: CoroutineScope,
): UDPConnection {
val modules = listOf(PingModule)
val modules = listOf(PacketModule, HandshakeModule, PingModule)
val context = createContext(
initialState = UDPConnectionState(
@@ -109,17 +193,20 @@ fun createUDPConnectionContext(
didHandshake = false,
address = remoteAddress.hostname,
port = remoteAddress.port,
deviceId = null,
),
reducers = modules.map { it.reducer },
scope = scope
scope = scope,
)
val dispatcher = PacketDispatcher()
val sendFunc = { packet: Packet ->
scope.launch {
val packetNum = context.state.value.lastPacketNum + 1
val bytePacket = buildPacket {
writePacket(this, packet)
writePacket(this, packet, packetNum)
}
socket.send(Datagram(bytePacket, remoteAddress))
}
@@ -127,9 +214,10 @@ fun createUDPConnectionContext(
}
val conn = UDPConnection(
context,
context = context,
serverContext = serverContext,
dispatcher,
send = sendFunc
send = sendFunc,
)
modules.map { it.observer }.forEach { it?.invoke(conn) }

View File

@@ -3,16 +3,34 @@ 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 kotlinx.io.Sink
import kotlinx.io.Source
import kotlinx.io.readByteArray
import kotlinx.io.readFloat
import kotlinx.io.writeFloat
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);
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 }
@@ -21,7 +39,12 @@ enum class PacketType(val id: Int) {
}
enum class UserAction(val id: Int) {
RESET_FULL(2), RESET_YAW(3), RESET_MOUNTING(4), PAUSE_TRACKING(5);
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]
@@ -29,11 +52,16 @@ enum class UserAction(val id: Int) {
}
sealed interface Packet
sealed interface SensorSpecificPacket : Packet { val sensorId: Int }
sealed interface RotationPacket : SensorSpecificPacket { val rotation: Quaternion }
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 HandshakeResponse(val nothing: Unit = Unit) : 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
@@ -59,7 +87,10 @@ private fun Source.readSafeQuat() = Quaternion(readFloat(), readFloat(), readFlo
}
private fun Sink.writeQuat(q: Quaternion) {
writeFloat(q.x); writeFloat(q.y); writeFloat(q.z); writeFloat(q.w)
writeFloat(q.x)
writeFloat(q.y)
writeFloat(q.z)
writeFloat(q.w)
}
private fun Source.readStr(len: Int) = buildString {
@@ -69,73 +100,144 @@ private fun Source.readStr(len: Int) = buildString {
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)
if (remaining >= 12) {
readInt()
readInt()
readInt()
}
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.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 -> {
is HandshakeResponse -> {
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 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 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 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) }
is ProtocolChange -> {
dst.writeU8(packet.targetProtocol)
dst.writeU8(packet.targetVersion)
}
else -> error("unhandled packet")
}
}
// ── 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) {
fun writePacket(dst: Sink, packet: Packet, packetNum: Long) {
val type = when (packet) {
is Heartbeat -> PacketType.HEARTBEAT
is Handshake -> PacketType.HANDSHAKE
is HandshakeResponse -> PacketType.HANDSHAKE
is Rotation -> PacketType.ROTATION
is Accel -> PacketType.ACCEL
is PingPong -> PacketType.PING_PONG
@@ -151,37 +253,47 @@ fun writePacket(dst: Sink, packet: Packet) {
is Temperature -> PacketType.TEMPERATURE
is UserActionPacket -> PacketType.USER_ACTION
is ProtocolChange -> PacketType.PROTOCOL_CHANGE
else -> error("unhandled packet")
}
if (type != PacketType.HANDSHAKE) {
dst.writeInt(type.id)
dst.writeLong(type.id.toLong()) // Sequence number placeholder
dst.writeLong(packetNum)
}
PacketCodec.write(dst, packet)
}
data class PacketEvent<out T : Packet>(
val data: T,
val packetNumber: Long,
)
class PacketDispatcher {
val listeners = mutableMapOf<KClass<out Packet>, MutableList<(Packet) -> Unit>>()
val listeners = mutableMapOf<KClass<out Packet>, MutableList<(PacketEvent<Packet>) -> Unit>>()
private val globalListeners = mutableListOf<(PacketEvent<Packet>) -> Unit>()
/**
* Listen for a specific packet type.
* Usage: dispatcher.on<Rotation> { packet -> println(packet.rotation) }
* Listen for a specific packet type with metadata.
*/
inline fun <reified T : Packet> on(crossinline callback: (T) -> Unit) {
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Packet> on(crossinline callback: (PacketEvent<T>) -> Unit) {
val list = listeners.getOrPut(T::class) { mutableListOf() }
synchronized(list) {
list.add { callback(it as T) }
list.add { callback(it as PacketEvent<T>) }
}
}
/**
* Broadcasts a packet to all registered listeners for its type.
*/
fun emit(packet: Packet) {
val list = listeners[packet::class] ?: return
fun onAny(callback: (PacketEvent<Packet>) -> Unit) {
synchronized(globalListeners) { globalListeners.add(callback) }
}
fun emit(event: PacketEvent<Packet>) {
synchronized(globalListeners) {
globalListeners.forEach { it(event) }
}
val list = listeners[event.data::class] ?: return
synchronized(list) {
list.forEach { it(packet) }
list.forEach { it(event) }
}
}
}

View File

@@ -1,5 +1,6 @@
package dev.slimevr.tracker.udp
import dev.slimevr.VRServer
import dev.slimevr.VRServerContext
import dev.slimevr.config.ConfigContext
import io.ktor.network.selector.SelectorManager
@@ -15,27 +16,18 @@ import kotlinx.coroutines.supervisorScope
data class UDPTrackerServerState(
val port: Int,
val connections: MutableMap<String, UDPConnection>
val connections: MutableMap<String, UDPConnection>,
)
suspend fun processPacket(
socket: BoundDatagramSocket,
datagram: Datagram,
workerId: Int
) {
}
const val PACKET_WORKERS = 4
suspend fun createUDPTrackerServer(
serverContext: VRServerContext,
configContext: ConfigContext
serverContext: VRServer,
configContext: ConfigContext,
): UDPTrackerServerState {
val state = UDPTrackerServerState(
port = configContext.state.value.settingsConfig.trackerPort,
connections = mutableMapOf()
connections = mutableMapOf(),
)
val selectorManager = SelectorManager(Dispatchers.IO)
@@ -54,29 +46,33 @@ suspend fun createUDPTrackerServer(
repeat(PACKET_WORKERS) { workerId ->
launch(Dispatchers.Default) {
for (datagram in packetChannel) {
val packet = readPacket(datagram.packet)
if (packet == null) {
println("null packet")
continue
}
val packetId = datagram.packet.readInt()
val packetNumber = datagram.packet.readLong()
val type = PacketType.fromId(packetId) ?: continue
val packetData = PacketCodec.read(type, datagram.packet)
val address = datagram.address as InetSocketAddress
val connContext = state.connections[address.hostname]
if (connContext !== null)
connContext.packetEvents.emit(packet = packet)
else {
val event = PacketEvent(
data = packetData,
packetNumber = packetNumber,
)
if (connContext !== null) {
connContext.packetEvents.emit(event = event)
} else {
val newContext = createUDPConnectionContext(
id = address.hostname,
remoteAddress = address,
socket = serverSocket,
scope = this
serverContext = serverContext,
scope = this,
)
state.connections[address.hostname] = newContext
newContext.packetEvents.emit(packet = packet)
newContext.packetEvents.emit(event = event)
}
}
}
}

View File

@@ -1,8 +1,9 @@
package dev.slimevr
import dev.slimevr.context.Context
import dev.slimevr.context.BasicModule
import dev.slimevr.context.Context
import dev.slimevr.context.createContext
import dev.slimevr.tracker.Device
import dev.slimevr.tracker.TrackerContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChangedBy
@@ -10,11 +11,14 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
data class VRServerState(
val handleId: Int,
val trackers: Map<Int, TrackerContext>,
val devices: Map<Int, Device>,
)
sealed interface VRServerActions {
data class NewTracker(val trackerId: Int, val context: TrackerContext) : VRServerActions
data class NewDevice(val deviceId: Int, val context: Device) : VRServerActions
}
typealias VRServerContext = Context<VRServerState, VRServerActions>
@@ -25,6 +29,12 @@ val TestModule = VRServerModule(
when (a) {
is VRServerActions.NewTracker -> s.copy(
trackers = s.trackers + (a.trackerId to a.context),
handleId = a.trackerId,
)
is VRServerActions.NewDevice -> s.copy(
devices = s.devices + (a.deviceId to a.context),
handleId = a.deviceId,
)
}
},
@@ -35,20 +45,34 @@ val TestModule = VRServerModule(
},
)
fun createVRServer(scope: CoroutineScope): VRServerContext {
val server = VRServerState(
trackers = mapOf(),
)
data class VRServer(
val context: VRServerContext,
) {
fun nextHandle() = context.state.value.handleId + 1
fun getTrackerContext(id: Int) = context.state.value.trackers[id]
fun getDeviceContext(id: Int) = context.state.value.devices[id]
val modules = listOf(TestModule)
companion object {
fun create(scope: CoroutineScope): VRServer {
val server = VRServerState(
handleId = 0,
trackers = mapOf(),
devices = mapOf(),
)
val context = createContext(
initialState = server,
reducers = modules.map { it.reducer },
scope = scope,
)
val modules = listOf(TestModule)
modules.map { it.observer }.forEach { it?.invoke(context) }
val context = createContext(
initialState = server,
reducers = modules.map { it.reducer },
scope = scope,
)
return context
modules.map { it.observer }.forEach { it?.invoke(context) }
return VRServer(
context = context,
)
}
}
}

View File

@@ -2,14 +2,14 @@
package dev.slimevr.desktop
import dev.slimevr.VRServer
import dev.slimevr.config.createConfig
import dev.slimevr.createVRServer
import dev.slimevr.tracker.udp.createUDPTrackerServer
import kotlinx.coroutines.runBlocking
fun main(args: Array<String>) = runBlocking {
val config = createConfig(this)
val server = createVRServer(this)
val server = VRServer.create(this)
val udpServer = createUDPTrackerServer(server, config)
}