Solarxr start

This commit is contained in:
loucass003
2026-03-19 17:43:15 +01:00
parent 4fd4997e60
commit 4c1e4691be
9 changed files with 179 additions and 112 deletions

View File

@@ -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")

View File

@@ -19,7 +19,6 @@ object AppLogger {
fromMinLevel(Level.INFO) {
toSink("stdout")
}
}
}
}

View File

@@ -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..<messageBundle.dataFeedMsgsLength) {
val header = messageBundle.dataFeedMsgs(index) ?: error("WIERD?")
println("HEADER: ${header.message()}")
// this.dataFeedHandler.onMessage(conn, header)
}
for (index in 0..<messageBundle.rpcMsgsLength) {
val header = messageBundle.rpcMsgs(index)
// this.rpcHandler.onMessage(conn, header)
}
for (index in 0..<messageBundle.pubSubMsgsLength) {
val header = messageBundle.pubSubMsgs(index)
// this.pubSubHandler.onMessage(conn, header)
}
}
fun createSolarXRWebsocketServer() {
embeddedServer(Netty, port = SOLARXR_PORT) {
install(WebSockets)
routing {
webSocket {
println("Client Connected!")
for (frame in incoming) {
when (frame) {
is Frame.Binary -> {
val data = frame.readBytes()
onSolarXRMessage(frame.buffer)
println("Received Binary Packet: ${data.size} bytes")
}
else -> {}
}
}
}
}
}.start(wait = true)
}

View File

@@ -0,0 +1,6 @@
package dev.slimevr.solarxr
data class SolarXRConnectionState(
val id: Int,
)

View File

@@ -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)
}
)

View File

@@ -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)
}
)

View File

@@ -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<PingPong> { paket ->
it.packetEvents.on<PingPong> { 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<BatteryLevel> { 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<SignalStrength> { 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<SensorInfo> { event ->
val tracker = it.getTracker(event.data.sensorId)
observer = { observerContext ->
observerContext.packetEvents.on<SensorInfo> { 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)

View File

@@ -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<out T : Packet>(
data class PacketEvent<out T : UDPPacket>(
val data: T,
val packetNumber: Long,
)
class PacketDispatcher {
val listeners = mutableMapOf<KClass<out Packet>, MutableList<(PacketEvent<Packet>) -> Unit>>()
private val globalListeners = mutableListOf<(PacketEvent<Packet>) -> Unit>()
val listeners = mutableMapOf<KClass<out UDPPacket>, MutableList<suspend (PacketEvent<UDPPacket>) -> Unit>>()
val globalListeners = mutableListOf<suspend (PacketEvent<UDPPacket>) -> Unit>()
val mutex = Mutex()
@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 PacketEvent<T>) }
inline fun <reified T : UDPPacket> on(crossinline callback: suspend (PacketEvent<T>) -> Unit) {
runBlocking {
mutex.withLock {
val list = listeners.getOrPut(T::class) { mutableListOf() }
list.add { callback(it as PacketEvent<T>) }
}
}
}
fun onAny(callback: (PacketEvent<Packet>) -> Unit) {
synchronized(globalListeners) { globalListeners.add(callback) }
fun onAny(callback: suspend (PacketEvent<UDPPacket>) -> Unit) {
runBlocking {
mutex.withLock { 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(event) }
suspend fun emit(event: PacketEvent<UDPPacket>) {
val targets = mutex.withLock {
val specific = listeners[event.data::class]?.toList() ?: emptyList()
val global = globalListeners.toList()
global + specific
}
targets.forEach { it(event) }
}
}

View File

@@ -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<String>) = runBlocking {
val config = createConfig(this)
val server = VRServer.create(this)
createUDPTrackerServer(server, config)
launch {
createUDPTrackerServer(server, config)
}
launch {
createSolarXRWebsocketServer()
}
Unit
}