feat: add SensorCollector for sensor data acquisition

This commit is contained in:
茂之钳
2026-05-25 07:04:23 +00:00
parent 95c7ac2e72
commit 912f1ea461
5 changed files with 246 additions and 7 deletions
@@ -0,0 +1,234 @@
package com.senstools.domain.sensor
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.core.content.ContextCompat
import com.senstools.domain.model.AttitudeData
import com.senstools.domain.model.GPSData
import com.senstools.domain.model.SensorData
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
interface SensorDataCallback {
fun onGPSData(gps: GPSData)
fun onSensorData(sensor: SensorData)
fun onAttitudeData(attitude: AttitudeData)
}
class SensorCollector {
private var sensorManager: SensorManager? = null
private var locationManager: LocationManager? = null
private var callback: SensorDataCallback? = null
private var collecting = false
// Sensors
private var accelerometer: android.hardware.Sensor? = null
private var gyroscope: android.hardware.Sensor? = null
private var magnetometer: android.hardware.Sensor? = null
private var pressureSensor: android.hardware.Sensor? = null
private var temperatureSensor: android.hardware.Sensor? = null
private var humiditySensor: android.hardware.Sensor? = null
// Sensor fusion for attitude
private val rotationMatrix = FloatArray(9)
private val orientationAngles = FloatArray(3)
private val gravity = FloatArray(3)
private val geomagnetic = FloatArray(3)
private val accelValues = FloatArray(3)
private val gyroValues = FloatArray(3)
private val magValues = FloatArray(3)
private val sensorEventListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
val timestamp = System.currentTimeMillis()
when (event.sensor.type) {
Sensor.TYPE_ACCELEROMETER -> {
System.arraycopy(event.values, 0, accelValues, 0, 3)
computeAttitude(timestamp)
emitSensorData(timestamp)
}
Sensor.TYPE_GYROSCOPE -> {
System.arraycopy(event.values, 0, gyroValues, 0, 3)
}
Sensor.TYPE_MAGNETIC_FIELD -> {
System.arraycopy(event.values, 0, magValues, 0, 3)
System.arraycopy(event.values, 0, geomagnetic, 0, 3)
}
Sensor.TYPE_PRESSURE -> {
// Pressure to altitude approximation: h = 44330 * (1 - (p/101325)^(1/5.255))
val pressure = event.values[0]
emitSensorDataWithPressure(timestamp, pressure)
}
Sensor.TYPE_AMBIENT_TEMPERATURE -> {
emitSensorDataWithTemp(timestamp, event.values[0])
}
Sensor.TYPE_RELATIVE_HUMIDITY -> {
emitSensorDataWithHumidity(timestamp, event.values[0])
}
}
}
override fun onAccuracyChanged(sensor: android.hardware.Sensor, accuracy: Int) {}
}
// Tracking variables for incremental sensor emission
private var lastAccelTimestamp = 0L
private var lastPressureTimestamp = 0L
private var lastTempTimestamp = 0L
private var lastHumidityTimestamp = 0L
private var pendingPressure = 0f
private var pendingTemp = 0f
private var pendingHumidity = 0f
private var hasPendingPressure = false
private var hasPendingTemp = false
private var hasPendingHumidity = false
private val locationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
val timestamp = System.currentTimeMillis()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val now = Date()
val gps = GPSData(
timestamp = timestamp,
longitude = location.longitude,
latitude = location.latitude,
altitude = location.altitude.toDouble(),
speed = location.speed,
bearing = location.bearing,
satellites = 0, // Not available from LocationListener directly
time = String.format("%tT", now),
date = String.format("%tF", now)
)
callback?.onGPSData(gps)
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
fun startCollecting(context: Context, cb: SensorDataCallback) {
if (collecting) return
callback = cb
collecting = true
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// Register sensors
sensorManager?.let { sm ->
accelerometer = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
gyroscope = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
magnetometer = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
pressureSensor = sm.getDefaultSensor(Sensor.TYPE_PRESSURE)
temperatureSensor = sm.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)
humiditySensor = sm.getDefaultSensor(Sensor.TYPE_RELATIVE_HUMIDITY)
// High-rate sensors: SENSOR_DELAY_GAME (~100Hz)
accelerometer?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_GAME) }
gyroscope?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_GAME) }
magnetometer?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_GAME) }
// Low-rate sensors: SENSOR_DELAY_NORMAL (~10Hz)
pressureSensor?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL) }
temperatureSensor?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL) }
humiditySensor?.let { sm.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL) }
}
// Register GPS
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
) {
locationManager?.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
200L, // 5Hz
0f,
locationListener,
Looper.getMainLooper()
)
}
}
fun stopCollecting() {
if (!collecting) return
collecting = false
sensorManager?.unregisterListener(sensorEventListener)
locationManager?.removeUpdates(locationListener)
sensorManager = null
locationManager = null
callback = null
}
fun isCollecting(): Boolean = collecting
private fun computeAttitude(timestamp: Long) {
// Simple attitude estimation from accelerometer + magnetometer
val g = FloatArray(3)
val m = FloatArray(3)
System.arraycopy(accelValues, 0, g, 0, 3)
System.arraycopy(magValues, 0, m, 0, 3)
if (!SensorManager.getRotationMatrix(rotationMatrix, null, g, m)) return
SensorManager.getOrientation(rotationMatrix, orientationAngles)
val pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat()
val roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
val yaw = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
callback?.onAttitudeData(
AttitudeData(
pitch = pitch,
roll = roll,
yaw = yaw,
timestamp = timestamp
)
)
}
private fun emitSensorData(timestamp: Long) {
lastAccelTimestamp = timestamp
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val now = Date()
callback?.onSensorData(
SensorData(
timestamp = timestamp,
accelerometer = accelValues.clone(),
gyroscope = gyroValues.clone(),
magnetometer = magValues.clone(),
pressureAltitude = if (hasPendingPressure) pendingPressure else 0f,
temperature = if (hasPendingTemp) pendingTemp else 0f,
humidity = if (hasPendingHumidity) pendingHumidity else 0f,
time = String.format("%tT", now),
date = String.format("%tF", now)
)
)
}
private fun emitSensorDataWithPressure(timestamp: Long, pressure: Float) {
hasPendingPressure = true
pendingPressure = 44330f * (1f - Math.pow((pressure / 101325f).toDouble(), 1.0 / 5.255).toFloat())
}
private fun emitSensorDataWithTemp(timestamp: Long, temp: Float) {
hasPendingTemp = true
pendingTemp = temp
}
private fun emitSensorDataWithHumidity(timestamp: Long, humidity: Float) {
hasPendingHumidity = true
pendingHumidity = humidity
}
}
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+9 -5
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -86,7 +86,8 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -114,6 +115,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -171,6 +173,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -203,14 +206,15 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
+2 -1
View File
@@ -70,10 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell