Skip to content

Commit 4850ee0

Browse files
committed
Separate step mounting from Tracker
1 parent 3b53e18 commit 4850ee0

File tree

8 files changed

+386
-216
lines changed

8 files changed

+386
-216
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package dev.slimevr.reset.accel
2+
3+
import dev.slimevr.tracking.trackers.Tracker
4+
import dev.slimevr.util.AccelAccumulator
5+
import io.eiren.util.logging.LogManager
6+
import io.github.axisangles.ktmath.Vector3
7+
import java.util.Timer
8+
import java.util.TimerTask
9+
import java.util.concurrent.locks.ReentrantLock
10+
import kotlin.concurrent.schedule
11+
import kotlin.concurrent.thread
12+
import kotlin.concurrent.withLock
13+
import kotlin.time.Duration.Companion.seconds
14+
import kotlin.time.DurationUnit
15+
import kotlin.time.TimeSource
16+
17+
// Handles recording and processing of acceleration-based session calibration
18+
class AccelResetHandler(val timeSource: TimeSource.WithComparableMarks = TimeSource.Monotonic) {
19+
var isRunning: Boolean = false
20+
private set
21+
var isDetecting: Boolean = false
22+
private set
23+
var isRecording: Boolean = false
24+
private set
25+
26+
private val recordingLock = ReentrantLock()
27+
28+
private var hmd: Tracker? = null
29+
private val trackers: MutableList<RecordingWrapper> = mutableListOf()
30+
31+
private val timeoutTimer = Timer()
32+
private var timerTask: TimerTask? = null
33+
34+
private var recStartTime = timeSource.markNow()
35+
36+
/**
37+
* Starts the accel reset process. performing rest detection on the trackers
38+
* provided to automatically control the recording period.
39+
*/
40+
fun start(hmd: Tracker, trackers: Iterable<Tracker>) = recordingLock.withLock {
41+
// Maybe should throw IllegalStateException? Or just restart?
42+
if (isRunning) return
43+
44+
// Nothing to do
45+
if (trackers.none()) return
46+
47+
// Initialize our state
48+
isRunning = true
49+
this.hmd = hmd
50+
51+
// Register our tracker event listener
52+
for (tracker in trackers) {
53+
val wrappedTracker = RecordingWrapper(tracker)
54+
this.trackers.add(wrappedTracker)
55+
tracker.accelTickCallback = {
56+
onAccelData(wrappedTracker)
57+
}
58+
}
59+
60+
// Start waiting for movement
61+
isDetecting = true
62+
timerTask?.cancel()
63+
timerTask = timeoutTimer.schedule(START_TIMEOUT.inWholeMilliseconds) {
64+
timeout()
65+
}
66+
67+
LogManager.info("[AccelResetHandler] Reset requested, detecting movement...")
68+
}
69+
70+
/**
71+
* Handles rest detection and data collection.
72+
*/
73+
private fun onAccelData(tracker: RecordingWrapper) {
74+
if (!isDetecting) return
75+
76+
val sample = tracker.makeSample(timeSource.markNow(), hmd?.position ?: Vector3.NULL)
77+
78+
// Rest detection
79+
tracker.updateRestState(sample)
80+
tracker.addRestSample(sample)
81+
// TODO: This shouldn't be done like this
82+
tracker.tracker.accelMountInProgress = isRecording && tracker.moving
83+
84+
if (!isRecording) {
85+
// We haven't started moving yet, don't start recording
86+
if (!tracker.moving) return
87+
88+
// Start recording
89+
recordingLock.withLock {
90+
// Race condition
91+
if (isRecording) return@withLock
92+
93+
// Dump rest detection into the recording on tracker threads
94+
for (tracker in trackers) tracker.dumpRest = true
95+
96+
isRecording = true
97+
recStartTime = timeSource.markNow()
98+
timerTask?.cancel()
99+
timerTask = timeoutTimer.schedule(RECORD_TIMEOUT.inWholeMilliseconds) {
100+
timeout()
101+
}
102+
103+
LogManager.info("[AccelResetHandler] Movement detected, recording started!")
104+
}
105+
} else if (
106+
timeSource.markNow() - recStartTime > MINIMUM_DURATION &&
107+
trackers.none { it.moving }
108+
) {
109+
// We're recording, the minimum duration has passed, and no trackers are
110+
// moving, therefore we can stop the recording and process it
111+
recordingLock.withLock {
112+
// Race condition
113+
if (!isRecording) return
114+
// Let's not block the tracker thread while processing
115+
thread {
116+
process()
117+
}
118+
}
119+
return
120+
}
121+
122+
// Take the latest sample or dump the rest detection samples into the recording
123+
if (!tracker.dumpRest) {
124+
tracker.recording.add(sample)
125+
} else {
126+
tracker.recording.addAll(tracker.restDetect)
127+
tracker.dumpRest = false
128+
}
129+
}
130+
131+
/**
132+
* Stops recording, processes the recorded data, then resets this handler.
133+
*/
134+
private fun process() {
135+
stop()
136+
137+
LogManager.info("[AccelResetHandler] Done recording, processing...")
138+
139+
for (tracker in trackers) {
140+
val firstSample = tracker.recording.first()
141+
val lastSample = tracker.recording.last()
142+
143+
// Compute the unbiased final velocity
144+
val calibAccum = AccelAccumulator()
145+
RecordingProcessor.processTimeline(calibAccum, tracker)
146+
147+
// Assume the final velocity is zero (at rest), we can divide our unbiased
148+
// final velocity (m/s) by the duration and get a static acceleration
149+
// offset (m/s^2)
150+
val duration = lastSample.time - firstSample.time
151+
val bias = calibAccum.velocity / duration.toDouble(DurationUnit.SECONDS).toFloat()
152+
153+
// Compute the biased final offset
154+
val finalAccum = AccelAccumulator()
155+
RecordingProcessor.processTimeline(finalAccum, tracker, accelBias = bias)
156+
157+
// Compute the final offsets
158+
val trackerOffset = finalAccum.offset
159+
val trackerXZ = Vector3(trackerOffset.x, 0f, trackerOffset.z)
160+
val hmdOffset = lastSample.hmdPos - firstSample.hmdPos
161+
val hmdXZ = Vector3(hmdOffset.x, 0f, hmdOffset.z)
162+
163+
// TODO: Fail on high error
164+
165+
// Compute mounting to fix the yaw offset from tracker to HMD
166+
val mountRot = RecordingProcessor.angle(trackerXZ.unit()) *
167+
RecordingProcessor.angle(hmdXZ.unit()).inv()
168+
169+
// Apply that mounting to the tracker
170+
val resetsHandler = tracker.tracker.resetsHandler
171+
val finalMounting = resetsHandler.mountingOrientation * resetsHandler.mountRotFix * mountRot
172+
resetsHandler.mountRotFix *= mountRot
173+
174+
LogManager.info(
175+
"[Accel] Tracker ${tracker.tracker.id} (${tracker.tracker.trackerPosition?.designation}):\n" +
176+
"Tracker offset: $trackerOffset\n" +
177+
"HMD offset: $hmdOffset\n" +
178+
"Error value (meters): ${trackerXZ.len() - hmdXZ.len()}\n" +
179+
"Resulting mounting: $finalMounting",
180+
)
181+
}
182+
183+
clean()
184+
}
185+
186+
/**
187+
* Stops recording without clearing the recorded data.
188+
*/
189+
private fun stop() = recordingLock.withLock {
190+
// Cancel any pending timeouts
191+
timerTask?.cancel()
192+
timerTask = null
193+
194+
isDetecting = false
195+
isRecording = false
196+
197+
// Unregister our tracker event listener
198+
for (tracker in trackers) {
199+
tracker.tracker.accelTickCallback = null
200+
tracker.tracker.accelMountInProgress = false
201+
}
202+
}
203+
204+
/**
205+
* Immediately stops execution and resets this handler.
206+
*/
207+
private fun clean() {
208+
stop()
209+
210+
// Reset data storage
211+
hmd = null
212+
trackers.clear()
213+
214+
isRunning = false
215+
}
216+
217+
/**
218+
* Stops the accel reset process and resets this handler.
219+
*/
220+
fun cancel() {
221+
clean()
222+
}
223+
224+
/**
225+
* Indicates that the process has timed out, then resets this handler.
226+
*/
227+
private fun timeout() {
228+
LogManager.warning("[AccelResetHandler] Reset timed out, aborting")
229+
clean()
230+
}
231+
232+
companion object {
233+
val START_TIMEOUT = 8.seconds
234+
val MINIMUM_DURATION = 2.seconds
235+
val RECORD_TIMEOUT = 8.seconds
236+
}
237+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dev.slimevr.reset.accel
2+
3+
import dev.slimevr.util.AccelAccumulator
4+
import io.github.axisangles.ktmath.Quaternion
5+
import io.github.axisangles.ktmath.Vector3
6+
import kotlin.math.atan2
7+
import kotlin.time.ComparableTimeMark
8+
import kotlin.time.Duration
9+
import kotlin.time.DurationUnit
10+
11+
object RecordingProcessor {
12+
fun accumSample(
13+
accum: AccelAccumulator,
14+
sample: RecordingSample,
15+
lastSampleTime: ComparableTimeMark? = null,
16+
accelBias: Vector3 = Vector3.NULL,
17+
): Duration {
18+
val delta = lastSampleTime?.let { sample.time - it } ?: Duration.ZERO
19+
accum.dataTick(sample.accel - accelBias, delta.toDouble(DurationUnit.SECONDS).toFloat())
20+
21+
return delta
22+
}
23+
24+
fun processTimeline(
25+
accum: AccelAccumulator,
26+
wrapper: RecordingWrapper,
27+
lastSampleTime: ComparableTimeMark? = null,
28+
accelBias: Vector3 = Vector3.NULL,
29+
): ComparableTimeMark? {
30+
var lastTime = lastSampleTime
31+
32+
for (sample in wrapper.recording) {
33+
accumSample(accum, sample, lastTime, accelBias)
34+
lastTime = sample.time
35+
}
36+
37+
return lastTime
38+
}
39+
40+
fun angle(vector: Vector3): Quaternion {
41+
val yaw = atan2(vector.x, vector.z)
42+
return Quaternion.rotationAroundYAxis(yaw)
43+
}
44+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.slimevr.reset.accel
2+
3+
import io.github.axisangles.ktmath.Quaternion
4+
import io.github.axisangles.ktmath.Vector3
5+
import kotlin.time.ComparableTimeMark
6+
7+
data class RecordingSample(
8+
val time: ComparableTimeMark,
9+
// Tracker
10+
val accel: Vector3,
11+
val rot: Quaternion,
12+
// HMD
13+
val hmdPos: Vector3,
14+
)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package dev.slimevr.reset.accel
2+
3+
import dev.slimevr.autobone.StatsCalculator
4+
import dev.slimevr.tracking.trackers.Tracker
5+
import io.github.axisangles.ktmath.Vector3
6+
import org.apache.commons.collections4.queue.CircularFifoQueue
7+
import kotlin.time.ComparableTimeMark
8+
import kotlin.time.Duration.Companion.milliseconds
9+
10+
data class RecordingWrapper(val tracker: Tracker, var moving: Boolean = false) {
11+
// Buffer for performing rest detection
12+
val restDetect = CircularFifoQueue<RecordingSample>(8)
13+
14+
// List capacity assuming ~10 seconds at 100 TPS
15+
val recording: MutableList<RecordingSample> = ArrayList(1024)
16+
17+
// Whether to dump our rest detection into the recording on the next sample
18+
var dumpRest = false
19+
20+
fun makeSample(time: ComparableTimeMark, hmdPos: Vector3): RecordingSample = RecordingSample(
21+
time,
22+
tracker.getAcceleration(),
23+
tracker.getRotation(),
24+
hmdPos,
25+
)
26+
27+
fun addRestSample(sample: RecordingSample): Boolean {
28+
// Collect samples for rest detection at a constant-ish rate if possible
29+
return if (moving && restDetect.isNotEmpty()) {
30+
val lastSampleTime = restDetect.last().time
31+
// Try to have TPS at a lower rate
32+
if (sample.time - lastSampleTime > REST_INTERVAL) {
33+
restDetect.add(sample)
34+
} else {
35+
false
36+
}
37+
} else {
38+
restDetect.add(sample)
39+
}
40+
}
41+
42+
fun updateRestState(new: RecordingSample): Boolean {
43+
if (restDetect.size < 4) return moving
44+
45+
val stats = StatsCalculator()
46+
for (sample in restDetect) {
47+
stats.addValue(sample.accel.len())
48+
}
49+
50+
// Conditions to start or remain moving
51+
// TODO: Add rotation as a rest metric
52+
moving = if (moving) {
53+
stats.mean >= 0.1f || stats.standardDeviation >= 0.2f
54+
} else {
55+
stats.mean >= 0.3f || new.accel.len() - stats.mean >= 0.6f
56+
}
57+
return moving
58+
}
59+
60+
companion object {
61+
val REST_INTERVAL = 100.milliseconds
62+
}
63+
}

0 commit comments

Comments
 (0)