feat: add SensorCollector for sensor data acquisition
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Vendored
+9
-5
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -86,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# 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.
|
||||
MAX_FD=maximum
|
||||
@@ -114,6 +115,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# 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
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -203,14 +206,15 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# 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.
|
||||
# * 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.
|
||||
|
||||
set -- \
|
||||
"-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.
|
||||
|
||||
Vendored
+2
-1
@@ -70,10 +70,11 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@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
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
Reference in New Issue
Block a user