diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 76c4b42dc2..99c197a757 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -139,6 +139,7 @@ tracker-status-error = Error tracker-status-disconnected = Disconnected tracker-status-occluded = Occluded tracker-status-ok = OK +tracker-status-timed_out = Timed out ## Tracker status columns tracker-table-column-name = Name diff --git a/gui/src/components/TopBar.tsx b/gui/src/components/TopBar.tsx index 9efd905bde..6079b44d11 100644 --- a/gui/src/components/TopBar.tsx +++ b/gui/src/components/TopBar.tsx @@ -5,6 +5,7 @@ import { RpcMessage, ServerInfosRequestT, ServerInfosResponseT, + TrackerStatus, } from 'solarxr-protocol'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import { CloseIcon } from './commons/icon/CloseIcon'; @@ -212,7 +213,9 @@ export function TopBar({ onClick={() => { if ( config?.connectedTrackersWarning && - connectedIMUTrackers.length > 0 + connectedIMUTrackers.filter( + (t) => t.tracker.status !== TrackerStatus.TIMED_OUT + ).length > 0 ) { setConnectedTrackerWarning(true); } else { diff --git a/gui/src/components/tracker/TrackerStatus.tsx b/gui/src/components/tracker/TrackerStatus.tsx index 5dc19f9895..5a9c90da09 100644 --- a/gui/src/components/tracker/TrackerStatus.tsx +++ b/gui/src/components/tracker/TrackerStatus.tsx @@ -11,6 +11,7 @@ const statusLabelMap: { [key: number]: string } = { [TrackerStatusEnum.DISCONNECTED]: 'tracker-status-disconnected', [TrackerStatusEnum.OCCLUDED]: 'tracker-status-occluded', [TrackerStatusEnum.OK]: 'tracker-status-ok', + [TrackerStatusEnum.TIMED_OUT]: 'tracker-status-timed_out', }; const statusClassMap: { [key: number]: string } = { @@ -20,6 +21,7 @@ const statusClassMap: { [key: number]: string } = { [TrackerStatusEnum.DISCONNECTED]: 'bg-background-30', [TrackerStatusEnum.OCCLUDED]: 'bg-status-warning', [TrackerStatusEnum.OK]: 'bg-status-success', + [TrackerStatusEnum.TIMED_OUT]: 'bg-status-warning', }; export function TrackerStatus({ status }: { status: number }) { diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index 4fb7babc6a..506312ec27 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -15,7 +15,8 @@ import solarxr_protocol.rpc.StatusTrackerErrorT import solarxr_protocol.rpc.StatusTrackerResetT import kotlin.properties.Delegates -const val TIMEOUT_MS = 2000L +const val TIMEOUT_MS = 2_000L +const val DISCONNECT_MS = 3_000L + TIMEOUT_MS /** * Generic tracker class for input and output tracker, @@ -65,7 +66,7 @@ class Tracker @JvmOverloads constructor( val needsMounting: Boolean = false, ) { private val timer = BufferedTimer(1f) - private var timeAtLastUpdate: Long = 0 + private var timeAtLastUpdate: Long = System.currentTimeMillis() private var rotation = Quaternion.IDENTITY private var acceleration = Vector3.NULL var position = Vector3.NULL @@ -254,8 +255,10 @@ class Tracker @JvmOverloads constructor( */ fun tick() { if (usesTimeout) { - if (System.currentTimeMillis() - timeAtLastUpdate > TIMEOUT_MS) { + if (System.currentTimeMillis() - timeAtLastUpdate > DISCONNECT_MS) { status = TrackerStatus.DISCONNECTED + } else if (System.currentTimeMillis() - timeAtLastUpdate > TIMEOUT_MS) { + status = TrackerStatus.TIMED_OUT } } filteringHandler.tick() @@ -270,6 +273,13 @@ class Tracker @JvmOverloads constructor( filteringHandler.dataTick(rotation) } + /** + * A way to delay the timeout of the tracker + */ + fun heartbeat() { + timeAtLastUpdate = System.currentTimeMillis() + } + /** * Gets the adjusted tracker rotation after all corrections * (filtering, reset, mounting and drift compensation). diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatus.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatus.kt index b9c3029640..3ba3973d9a 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatus.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerStatus.kt @@ -7,11 +7,12 @@ enum class TrackerStatus(val id: Int, val sendData: Boolean, val reset: Boolean) BUSY(2, true, false), ERROR(3, false, true), OCCLUDED(4, false, false), + TIMED_OUT(5, false, false), ; companion object { - private val byId = values().associateBy { it.id } + private val byId = entries.associateBy { it.id } @JvmStatic fun getById(id: Int): TrackerStatus? = byId[id] diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt index d41057cebf..ec32b6e424 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt @@ -5,7 +5,10 @@ object TrackerUtils { /** * Finds a suitable tracker for use in the SlimeVR skeleton * in allTrackers matching the position. - * This won't return disconnected, errored or internal trackers. + * + * This won't return disconnected, errored or internal trackers, + * but it will return timed out trackers, and they have a lower + * priority than normal trackers. * * @return A tracker for use in the SlimeVR skeleton */ @@ -19,50 +22,64 @@ object TrackerUtils { /** * Finds the first non-internal tracker from allTrackers - * matching the position, that is not DISCONNECTED + * matching the position, that is not `TrackerStatus.reset`. + * It will also choose timed out trackers, but they have a lower + * priority than the rest of the trackers * * @return A non-internal tracker */ private fun getNonInternalTrackerForBodyPosition( allTrackers: List, position: TrackerPosition, - ): Tracker? = allTrackers.firstOrNull { - it.trackerPosition === position && - !it.isInternal && - !it.status.reset + ): Tracker? { + val resetTrackers = allTrackers.filter { + it.trackerPosition === position && + !it.isInternal && + !it.status.reset + } + return resetTrackers.firstOrNull { it.status != TrackerStatus.TIMED_OUT } ?: resetTrackers.firstOrNull() } /** * Finds the first non-internal non-computed tracker from allTrackers - * matching the position, that is not DISCONNECTED. + * matching the position, that is not `TrackerStatus.reset`. + * It will also choose timed out trackers, but they have a lower + * priority than the rest of the trackers * * @return A non-internal non-computed tracker */ private fun getNonInternalNonComputedTrackerForBodyPosition( allTrackers: List, position: TrackerPosition, - ): Tracker? = allTrackers.firstOrNull { - it.trackerPosition === position && - !it.isComputed && - !it.isInternal && - !it.status.reset + ): Tracker? { + val resetTrackers = allTrackers.filter { + it.trackerPosition === position && + !it.isComputed && + !it.isInternal && + !it.status.reset + } + return resetTrackers.firstOrNull { it.status != TrackerStatus.TIMED_OUT } ?: resetTrackers.firstOrNull() } /** * Finds the first non-internal and non-imu tracker from allTrackers - * matching the position, that is not DISCONNECTED. - * + * matching the position, that is not `TrackerStatus.reset`. + * It will also choose timed out trackers, but they have a lower + * priority than the rest of the trackers * @return A non-internal non-imu tracker */ @JvmStatic fun getNonInternalNonImuTrackerForBodyPosition( allTrackers: List, position: TrackerPosition, - ): Tracker? = allTrackers.firstOrNull { - it.trackerPosition === position && - !it.isImu() && - !it.isInternal && - !it.status.reset + ): Tracker? { + val resetTrackers = allTrackers.filter { + it.trackerPosition === position && + !it.isImu() && + !it.isInternal && + !it.status.reset + } + return resetTrackers.firstOrNull { it.status != TrackerStatus.TIMED_OUT } ?: resetTrackers.firstOrNull() } /** diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt index ae8669e53d..2a08f00508 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt @@ -188,7 +188,8 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker imuType = sensorType, allowFiltering = true, needsReset = true, - needsMounting = true + needsMounting = true, + usesTimeout = true ) connection.trackers[trackerId] = imuTracker trackersConsumer.accept(imuTracker) @@ -244,16 +245,21 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker parser.write(bb, conn, UDPPacket1Heartbeat) socket.send(DatagramPacket(rcvBuffer, bb.position(), conn.address)) if (conn.lastPacket + 1000 < System.currentTimeMillis()) { - for (value in conn.trackers.values) { - value.status = TrackerStatus.DISCONNECTED - } if (!conn.timedOut) { conn.timedOut = true LogManager.info("[TrackerServer] Tracker timed out: $conn") } } else { + for (value in conn.trackers.values) { + if (value.status == TrackerStatus.DISCONNECTED || + value.status == TrackerStatus.TIMED_OUT + ) { + value.status = TrackerStatus.OK + } + } conn.timedOut = false } + if (conn.serialBuffer.isNotEmpty() && conn.lastSerialUpdate + 500L < System.currentTimeMillis() ) { @@ -266,6 +272,7 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker serialBuffer2.setLength(0) conn.serialBuffer.setLength(0) } + if (conn.lastPingPacketTime + 500 < System.currentTimeMillis()) { conn.lastPingPacketId = random.nextInt() conn.lastPingPacketTime = System.currentTimeMillis() diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPProtocolParser.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPProtocolParser.kt index c75e295497..674f018499 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPProtocolParser.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPProtocolParser.kt @@ -17,6 +17,9 @@ class UDPProtocolParser { ) } connection.lastPacket = System.currentTimeMillis() + connection.trackers.forEach { (_, tracker) -> + tracker.heartbeat() + } } if (packetId == PACKET_BUNDLE) { bundlePackets.clear() diff --git a/solarxr-protocol b/solarxr-protocol index cec1f04c48..8910f60c6a 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit cec1f04c48d3fd6a999225b656ea616aca2f23e5 +Subproject commit 8910f60c6ae07c9c865e7eb50f9ed4c490b1e822