Repository navigation
Motion Sensor Tutorial
The specialty topic of focus in our project is using the integrated motion sensor in android phones. Specifially, we chose to use the integrated gyroscope to calculate all of the needed data relating to squatting. Our demo app uses the gyroscope to detect a series of phases during the squat, which allows rep count, duration, average speed, and lowest depth to be calculated effectively. The idea of this demo app is to allow people to track basic squat data easily and quickly, and save the data for later comparison.
Requirements for current development:
Android Studio Narwhal 3 Feature Drop | 2025.1.3 https://developer.android.com/studio/releases/past-releases/as-narwhal-3-feature-drop-release-notes Gradle 8.13 Kotlin 1.9+ Android SDK 24+ Phone with gyroscope sensor
Development instructions: The gyroscope functionality should be declared in a viewmodel, and basic creation and functions should resemble something close to the following:
class TrackingScreenViewModel(app: Application) : AndroidViewModel(app), SensorEventListener {
private val _gyro = MutableStateFlow(FloatArray(3))
val gyro = _gyro.asStateFlow()
fun startSensors() {
accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
accelSensor?.let {
sensorManager.registerListener(
this,
it,
SensorManager.SENSOR_DELAY_GAME
)
}
gyroSensor?.let {
sensorManager.registerListener(
this,
it,
SensorManager.SENSOR_DELAY_GAME
)
}
}
fun stopSensors() {
sensorManager.unregisterListener(this)
}
}
This allows sensors to be enabled and disabled when called, and allows views to access data sourced from the sensors.
As for our squat state detection system, the main logic is as follows:
override fun onSensorChanged(event: SensorEvent) {
when (event.sensor.type) {
Sensor.TYPE_GYROSCOPE -> {
_gyro.value = event.values.clone()
// Integrate gyroscope data to get pitch angle
if (_isTracking.value && !_isCalibrating.value) {
val currentTime = event.timestamp
if (lastGyroTimestamp != 0L) {
// Calculate time difference in seconds
val dt = (currentTime - lastGyroTimestamp) / 1_000_000_000f
// Get angular velocity around X-axis (pitch rotation)
val angularVelocityX = event.values[0] // rad/s
// Convert to degrees and calculate angle change
val angleChange = Math.toDegrees(angularVelocityX.toDouble()).toFloat() * dt
// Integrate to get total pitch angle
integratedPitch += angleChange
//set the current depth
_currentDepth.value = abs(integratedPitch - baselineAngle)
lastGyroTimestamp = currentTime
println("Integrated pitch: $integratedPitch, angular velocity: $angularVelocityX rad/s")
processRepData(integratedPitch)
}
lastGyroTimestamp = currentTime
}
}
}
}
This method reads gyroscope updates, converts angular velocity into an actual pitch angle, and calculates how far the user has moved from their calibrated starting position. This pitch angle is then used to detect phases of each rep and records rep data such as depth and duration.