This commit is contained in:
loucass003
2026-03-27 05:58:46 +01:00
parent 38743dc8b8
commit df4569fe17
34 changed files with 145 additions and 119 deletions

View File

@@ -15,4 +15,4 @@ object BaseBehaviour : VRServerBehaviour {
println("tracker list size changed")
}.launchIn(receiver.context.scope)
}
}
}

View File

@@ -19,4 +19,4 @@ object DefaultUserBehaviour : UserConfigBehaviour {
is UserConfigActions.Update -> state.copy(data = action.transform(state.data))
is UserConfigActions.LoadProfile -> action.newState
}
}
}

View File

@@ -87,4 +87,4 @@ class AppConfig(
)
}
}
}
}

View File

@@ -41,4 +41,4 @@ class Context<S, in A>(
return Context(mutableStateFlow, applyAction, scope)
}
}
}
}

View File

@@ -4,12 +4,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
object DeviceStatsBehaviour : DeviceBehaviour {
override fun reduce(state: DeviceState, action: DeviceActions) =
if (action is DeviceActions.Update) action.transform(state) else state
override fun reduce(state: DeviceState, action: DeviceActions) = if (action is DeviceActions.Update) action.transform(state) else state
override fun observe(receiver: DeviceContext) {
receiver.state.onEach {
// AppLogger.device.info("Device state changed", it)
}.launchIn(receiver.scope)
}
}
}

View File

@@ -73,4 +73,4 @@ class Device(
return Device(context = context)
}
}
}
}

View File

@@ -3,16 +3,17 @@ package dev.slimevr.firmware
object FirmwareManagerBaseBehaviour : FirmwareManagerBehaviour {
override fun reduce(state: FirmwareManagerState, action: FirmwareManagerActions) = when (action) {
is FirmwareManagerActions.UpdateJob -> state.copy(
jobs = state.jobs + (
action.portLocation to FirmwareJobStatus(
portLocation = action.portLocation,
firmwareDeviceId = action.firmwareDeviceId,
status = action.status,
progress = action.progress,
)
),
jobs = state.jobs +
(
action.portLocation to FirmwareJobStatus(
portLocation = action.portLocation,
firmwareDeviceId = action.firmwareDeviceId,
status = action.status,
progress = action.progress,
)
),
)
is FirmwareManagerActions.RemoveJob -> state.copy(jobs = state.jobs - action.portLocation)
}
}
}

View File

@@ -124,4 +124,4 @@ class FirmwareManager(
return manager
}
}
}
}

View File

@@ -12,13 +12,17 @@ import solarxr_protocol.datatypes.TrackerStatus
object HIDRegistrationBehaviour : HIDReceiverBehaviour {
override fun reduce(state: HIDReceiverState, action: HIDReceiverActions) = when (action) {
is HIDReceiverActions.DeviceRegistered -> state.copy(
trackers = state.trackers + (action.hidId to HIDTrackerRecord(
hidId = action.hidId,
address = action.address,
deviceId = action.deviceId,
trackerId = null,
)),
trackers = state.trackers +
(
action.hidId to HIDTrackerRecord(
hidId = action.hidId,
address = action.address,
deviceId = action.deviceId,
trackerId = null,
)
),
)
else -> state
}
@@ -59,6 +63,7 @@ object HIDDeviceInfoBehaviour : HIDReceiverBehaviour {
val existing = state.trackers[action.hidId] ?: return state
state.copy(trackers = state.trackers + (action.hidId to existing.copy(trackerId = action.trackerId)))
}
else -> state
}
@@ -163,4 +168,4 @@ object HIDStatusBehaviour : HIDReceiverBehaviour {
)
}
}
}
}

View File

@@ -117,4 +117,4 @@ class HIDReceiver(
return receiver
}
}
}
}

View File

@@ -77,8 +77,7 @@ data class HIDRotationButton(
val rssi: Int,
) : HIDPacket
private fun readLE16Signed(data: ByteArray, offset: Int): Int =
data[offset + 1].toInt() shl 8 or data[offset].toUByte().toInt()
private fun readLE16Signed(data: ByteArray, offset: Int): Int = data[offset + 1].toInt() shl 8 or data[offset].toUByte().toInt()
private fun decodeQ15Quat(data: ByteArray, offset: Int): Quaternion {
val scale = 1f / 32768f

View File

@@ -27,4 +27,4 @@ object SerialLogBehaviour : SerialConnectionBehaviour {
SerialConnectionActions.Disconnected -> state.copy(connected = false)
}
}
}

View File

@@ -52,4 +52,4 @@ sealed interface SerialConnection {
}
data object Flashing : SerialConnection
}
}

View File

@@ -123,4 +123,4 @@ class SerialServer(
return server
}
}
}
}

View File

@@ -147,4 +147,4 @@ object DataFeedInitBehaviour : SolarXRConnectionBehaviour {
receiver.send(fbb.dataBuffer().moveToByteArray())
}
}
}
}

View File

@@ -69,4 +69,4 @@ class FirmwareBehaviour(private val firmwareManager: FirmwareManager) : SolarXRC
firmwareManager.cancelAll()
}
}
}
}

View File

@@ -129,4 +129,4 @@ class SerialBehaviour(private val serialServer: SerialServer) : SolarXRConnectio
if (c is SerialConnection.Console) c.handle.writeCommand(command)
}
}
}
}

View File

@@ -44,4 +44,4 @@ class VrcBehaviour(
vrcManager.context.dispatch(VRCConfigActions.ToggleMutedWarning(key))
}
}
}
}

View File

@@ -4,12 +4,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
object TrackerInfosBehaviour : TrackerBehaviour {
override fun reduce(state: TrackerState, action: TrackerActions) =
if (action is TrackerActions.Update) action.transform(state) else state
override fun reduce(state: TrackerState, action: TrackerActions) = if (action is TrackerActions.Update) action.transform(state) else state
override fun observe(receiver: TrackerContext) {
receiver.state.onEach {
// AppLogger.tracker.info("Tracker state changed {State}", it)
}.launchIn(receiver.scope)
}
}
}

View File

@@ -59,4 +59,4 @@ class Tracker(
return Tracker(context = context)
}
}
}
}

View File

@@ -22,6 +22,7 @@ object PacketBehaviour : UDPConnectionBehaviour {
if (action.packetNum != null) newState = newState.copy(lastPacketNum = action.packetNum)
newState
}
else -> state
}
@@ -225,4 +226,4 @@ object SensorRotationBehaviour : UDPConnectionBehaviour {
tracker.context.dispatch(TrackerActions.Update { copy(rawRotation = event.data.rotation) })
}
}
}
}

View File

@@ -144,4 +144,4 @@ class UDPConnection(
return conn
}
}
}
}

View File

@@ -9,18 +9,25 @@ import kotlinx.coroutines.flow.onEach
object DefaultVRCConfigBehaviour : VRCConfigBehaviour {
override fun reduce(state: VRCConfigState, action: VRCConfigActions) = when (action) {
is VRCConfigActions.UpdateValues -> state.copy(currentValues = action.values)
is VRCConfigActions.ToggleMutedWarning -> {
if (action.key !in VRC_VALID_KEYS) state
else if (action.key in state.mutedWarnings) state.copy(mutedWarnings = state.mutedWarnings - action.key)
else state.copy(mutedWarnings = state.mutedWarnings + action.key)
if (action.key !in VRC_VALID_KEYS) {
state
} else if (action.key in state.mutedWarnings) {
state.copy(mutedWarnings = state.mutedWarnings - action.key)
} else {
state.copy(mutedWarnings = state.mutedWarnings + action.key)
}
}
}
override fun observe(receiver: VRCConfigManager) {
receiver.context.state.map { it.mutedWarnings }.distinctUntilChanged().onEach { warnings ->
receiver.config.settings.context.dispatch(SettingsActions.Update {
copy(mutedVRCWarnings = warnings)
})
receiver.config.settings.context.dispatch(
SettingsActions.Update {
copy(mutedVRCWarnings = warnings)
},
)
}.launchIn(receiver.context.scope)
}
}
}

View File

@@ -94,8 +94,8 @@ fun computeRecommendedValues(server: VRServer, userHeight: Double): VRCConfigRec
return VRCConfigRecommendedValues(
legacyMode = false,
shoulderTrackingDisabled =
(!hasLeftHandWithPosition || !hasRightHandWithPosition || isMissingAnArmTracker) &&
((hasLeftHandWithPosition && hasRightHandWithPosition) || isMissingAShoulderTracker),
(!hasLeftHandWithPosition || !hasRightHandWithPosition || isMissingAnArmTracker) &&
((hasLeftHandWithPosition && hasRightHandWithPosition) || isMissingAShoulderTracker),
userHeight = userHeight.toFloat(),
calibrationRange = 0.2f,
trackerModel = VRCTrackerModel.AXIS,
@@ -106,15 +106,14 @@ fun computeRecommendedValues(server: VRServer, userHeight: Double): VRCConfigRec
)
}
fun computeValidity(values: VRCConfigValues, recommended: VRCConfigRecommendedValues): VRCConfigValidity =
VRCConfigValidity(
legacyModeOk = values.legacyMode == recommended.legacyMode,
shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled,
spineModeOk = recommended.spineMode?.contains(values.spineMode) == true,
trackerModelOk = values.trackerModel == recommended.trackerModel,
calibrationRangeOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1f,
userHeightOk = abs(recommended.userHeight - values.userHeight) < 0.1f,
calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals,
avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType,
shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation,
)
fun computeValidity(values: VRCConfigValues, recommended: VRCConfigRecommendedValues): VRCConfigValidity = VRCConfigValidity(
legacyModeOk = values.legacyMode == recommended.legacyMode,
shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled,
spineModeOk = recommended.spineMode?.contains(values.spineMode) == true,
trackerModelOk = values.trackerModel == recommended.trackerModel,
calibrationRangeOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1f,
userHeightOk = abs(recommended.userHeight - values.userHeight) < 0.1f,
calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals,
avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType,
shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation,
)

View File

@@ -24,4 +24,4 @@ fun buildTestSerialServer(scope: CoroutineScope) = SerialServer.create(
scope = scope,
)
fun buildTestVrServer(scope: CoroutineScope): VRServer = VRServer.create(scope)
fun buildTestVrServer(scope: CoroutineScope): VRServer = VRServer.create(scope)

View File

@@ -3,12 +3,12 @@ package dev.slimevr.firmware
import dev.llelievr.espflashkotlin.FlasherSerialInterface
import dev.slimevr.VRServer
import dev.slimevr.VRServerActions
import dev.slimevr.device.Device
import dev.slimevr.device.DeviceActions
import dev.slimevr.device.DeviceOrigin
import dev.slimevr.serial.SerialPortHandle
import dev.slimevr.serial.SerialPortInfo
import dev.slimevr.serial.SerialServer
import dev.slimevr.device.DeviceOrigin
import dev.slimevr.device.Device
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -59,9 +59,7 @@ private fun buildSerialServer(
// UncompletedCoroutinesError when the test ends.
private fun buildVrServer(
backgroundScope: kotlinx.coroutines.CoroutineScope,
): VRServer {
return VRServer.create(backgroundScope)
}
): VRServer = VRServer.create(backgroundScope)
class DoSerialFlashTest {

View File

@@ -10,13 +10,12 @@ import solarxr_protocol.data_feed.StartDataFeed
import kotlin.test.Test
import kotlin.test.assertEquals
private fun testConn(backgroundScope: kotlinx.coroutines.CoroutineScope, onSend: suspend (ByteArray) -> Unit) =
SolarXRConnection.create(
buildTestVrServer(backgroundScope),
onSend = onSend,
scope = backgroundScope,
behaviours = listOf(DataFeedInitBehaviour),
)
private fun testConn(backgroundScope: kotlinx.coroutines.CoroutineScope, onSend: suspend (ByteArray) -> Unit) = SolarXRConnection.create(
buildTestVrServer(backgroundScope),
onSend = onSend,
scope = backgroundScope,
behaviours = listOf(DataFeedInitBehaviour),
)
private fun config(intervalMs: Int) = DataFeedConfig(minimumTimeSinceLast = intervalMs.toUShort())

View File

@@ -46,4 +46,4 @@ fun main(args: Array<String>) = runBlocking {
launch { createIpcServers(server, solarXRBehaviours) }
Unit
}
}

View File

@@ -2,10 +2,10 @@ package dev.slimevr.desktop.hid
import dev.slimevr.AppLogger
import dev.slimevr.VRServer
import dev.slimevr.hid.HIDReceiver
import dev.slimevr.hid.HID_TRACKER_PID
import dev.slimevr.hid.HID_TRACKER_RECEIVER_PID
import dev.slimevr.hid.HID_TRACKER_RECEIVER_VID
import dev.slimevr.hid.HIDReceiver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -23,8 +23,7 @@ import org.hid4java.jna.HidDeviceInfoStructure
private const val HID_POLL_INTERVAL_MS = 3000L
private fun isCompatibleDevice(vid: Int, pid: Int) =
vid == HID_TRACKER_RECEIVER_VID && (pid == HID_TRACKER_RECEIVER_PID || pid == HID_TRACKER_PID)
private fun isCompatibleDevice(vid: Int, pid: Int) = vid == HID_TRACKER_RECEIVER_VID && (pid == HID_TRACKER_RECEIVER_PID || pid == HID_TRACKER_PID)
private val hidSpec = HidServicesSpecification().apply { isAutoStart = false }
@@ -32,7 +31,7 @@ private val hidSpec = HidServicesSpecification().apply { isAutoStart = false }
private val hidServices by lazy { HidManager.getHidServices(hidSpec) }
private fun enumerateCompatibleDevices(): Map<String, HidDevice> {
hidServices // ensure native lib is loaded
hidServices // ensure native lib is loaded
val root = HidApi.enumerateDevices(0, 0) ?: return emptyMap()
val result = mutableMapOf<String, HidDevice>()
var info: HidDeviceInfoStructure? = root
@@ -56,7 +55,11 @@ fun createDesktopHIDManager(serverContext: VRServer, scope: CoroutineScope) {
scope.launch {
while (isActive) {
val found = withContext(Dispatchers.IO) {
try { enumerateCompatibleDevices() } catch (_: Exception) { emptyMap() }
try {
enumerateCompatibleDevices()
} catch (_: Exception) {
emptyMap()
}
}
// Devices no longer present + jobs that exited on their own (read error)
@@ -88,12 +91,19 @@ fun createDesktopHIDManager(serverContext: VRServer, scope: CoroutineScope) {
try {
while (isActive) {
val data = withContext(Dispatchers.IO) {
try { hidDevice.readAll(0) } catch (_: Exception) { null }
try {
hidDevice.readAll(0)
} catch (_: Exception) {
null
}
}
when {
data == null -> return@channelFlow // read error, device gone
data == null -> return@channelFlow
// read error, device gone
data.isNotEmpty() -> send(data)
else -> delay(1) // no data yet, yield without busy-spinning
else -> delay(1) // no data yet, yield without busy-spinning
}
}
} finally {

View File

@@ -38,7 +38,7 @@ suspend fun createUnixSolarXRSocket(server: VRServer, behaviours: List<SolarXRCo
server = server,
messages = readFramedMessages(channel),
send = { bytes -> withContext(Dispatchers.IO) { writeFramed(channel, bytes) } },
behaviours = behaviours
behaviours = behaviours,
)
}

View File

@@ -6,14 +6,14 @@ import dev.slimevr.desktop.platform.Position
import dev.slimevr.desktop.platform.ProtobufMessage
import dev.slimevr.desktop.platform.TrackerAdded
import dev.slimevr.desktop.platform.Version
import dev.slimevr.solarxr.SolarXRConnectionBehaviour
import dev.slimevr.solarxr.SolarXRConnection
import dev.slimevr.solarxr.onSolarXRMessage
import dev.slimevr.device.Device
import dev.slimevr.device.DeviceActions
import dev.slimevr.device.DeviceOrigin
import dev.slimevr.tracker.TrackerActions
import dev.slimevr.device.Device
import dev.slimevr.solarxr.SolarXRConnection
import dev.slimevr.solarxr.SolarXRConnectionBehaviour
import dev.slimevr.solarxr.onSolarXRMessage
import dev.slimevr.tracker.Tracker
import dev.slimevr.tracker.TrackerActions
import io.github.axisangles.ktmath.Quaternion
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow

View File

@@ -41,7 +41,7 @@ suspend fun createWindowsSolarXRPipe(server: VRServer, behaviours: List<SolarXRC
server = server,
messages = readFramedMessages(handle),
send = { bytes -> withContext(Dispatchers.IO) { writeFramedPipe(handle, bytes) } },
behaviours = behaviours
behaviours = behaviours,
)
}

View File

@@ -18,7 +18,10 @@ import kotlin.io.path.exists
private const val USER_REG_SUBPATH = "steamapps/compatdata/438100/pfx/user.reg"
private val KEY_VALUE_PATTERN = Regex(""""(.+)"=(.+)""")
private val HEX_FORMAT = HexFormat { upperCase = false; bytes.byteSeparator = "," }
private val HEX_FORMAT = HexFormat {
upperCase = false
bytes.byteSeparator = ","
}
internal val linuxUserRegPath = System.getenv("HOME")?.let { home ->
listOf(
@@ -70,8 +73,11 @@ internal suspend fun linuxGetQwordValue(registry: Map<String, String>, key: Stri
internal suspend fun linuxGetDwordValue(registry: Map<String, String>, key: String): Int? = try {
val value = registry[key] ?: return null
if (value.startsWith("dword:")) value.substring(6).toInt(16)
else throw InvalidObjectException("Expected DWORD but got: $value")
if (value.startsWith("dword:")) {
value.substring(6).toInt(16)
} else {
throw InvalidObjectException("Expected DWORD but got: $value")
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
@@ -92,10 +98,12 @@ internal fun linuxVRCConfigFlow(): Flow<solarxr_protocol.rpc.VRCConfigValues?> =
if (keys.isEmpty()) {
emit(null)
} else {
emit(buildVRCConfigValues(
intValue = { key -> keys[key]?.let { linuxGetDwordValue(registry, it) } },
doubleValue = { key -> keys[key]?.let { linuxGetQwordValue(registry, it) } },
))
emit(
buildVRCConfigValues(
intValue = { key -> keys[key]?.let { linuxGetDwordValue(registry, it) } },
doubleValue = { key -> keys[key]?.let { linuxGetQwordValue(registry, it) } },
),
)
}
delay(3000)
// it seems that on linux, steam writes to the reg file is unpredictable.

View File

@@ -13,27 +13,28 @@ import solarxr_protocol.rpc.VRCTrackerModel
internal const val VRC_REG_PATH = "Software\\VRChat\\VRChat"
fun createDesktopVRCConfigManager(config: AppConfig, scope: CoroutineScope): VRCConfigManager =
when (CURRENT_PLATFORM) {
Platform.WINDOWS -> VRCConfigManager.create(
config = config,
scope = scope,
isSupported = true,
values = windowsVRCConfigFlow(),
)
Platform.LINUX -> VRCConfigManager.create(
config = config,
scope = scope,
isSupported = true,
values = linuxVRCConfigFlow(),
)
else -> VRCConfigManager.create(
config = config,
scope = scope,
isSupported = false,
values = emptyFlow(),
)
}
fun createDesktopVRCConfigManager(config: AppConfig, scope: CoroutineScope): VRCConfigManager = when (CURRENT_PLATFORM) {
Platform.WINDOWS -> VRCConfigManager.create(
config = config,
scope = scope,
isSupported = true,
values = windowsVRCConfigFlow(),
)
Platform.LINUX -> VRCConfigManager.create(
config = config,
scope = scope,
isSupported = true,
values = linuxVRCConfigFlow(),
)
else -> VRCConfigManager.create(
config = config,
scope = scope,
isSupported = false,
values = emptyFlow(),
)
}
internal suspend fun buildVRCConfigValues(
intValue: suspend (String) -> Int?,