mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
basic handshake
This commit is contained in:
@@ -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(
|
||||
|
||||
86
server/core/src/main/java/dev/slimevr/tracker/device.kt
Normal file
86
server/core/src/main/java/dev/slimevr/tracker/device.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user